MongoDB分片+副本集群模式部署

前言

  • 近期因产品升级,业务扩展的需要,将MongoDB单机改换成集群模式,于是配合运维和测试的小伙伴一起验证MongDB集群,由于都没有使用过分片集群,而刚好我前一份工作中构搭建和应用过MongoDB副本集群,这次一起将副本+分片集群从头部署了一遍,将部署的整个过程记录如下。

    1.集群环境

  • 集群模式: 分片+副本
  • 操作系统: Linux(以CentOS 6.5为例)
  • 硬件:3台服务器(或3个VM)

    2.拓扑图

    image

    3.安装MongDB

  • 创建/etc/yum.repos.d/mongodb-org-3.4.repo文件

    1
    2
    3
    4
    5
    6
    [mongodb-org-3.4]
    name=MongoDB Repository
    baseurl=https://repo.mongodb.org/yum/redhat/$releasever/mongodb-org/3.4/x86_64/
    gpgcheck=1
    enabled=1
    gpgkey=https://www.mongodb.org/static/pgp/server-3.4.asc
  • 执行安装命令

    1
    sudo yum install -y mongodb-org
  • 在三台服务器上分别执行以上操作,安装好MongoDB

参考链接:https://docs.mongodb.com/manual/tutorial/install-mongodb-on-red-hat/

4.配置集群

4.1创建节点目录

1
2
mkdir -p /var/lib/mongodb/conf
mkdir -p /var/lib/mongodb/data/{shard1,shard2,shard3,config}

4.1创建节点配置

  • 进入/var/lib/mongodb/conf目录下

    1
    cd /var/lib/mongodb/conf
  • 把附件中的配置文件copy在/var/lib/mongodb/conf目录下

  • 将/var/lib/mongodb结点目录复制到其它两台服务器中,目录一至
  • 分别启动三台服务器中的结点

    1
    2
    3
    4
    numactl --interleave=all mongod -f shard1.conf
    numactl --interleave=all mongod -f shard2.conf
    numactl --interleave=all mongod -f shard3.conf
    numactl --interleave=all mongod -f configdb.conf
  • 等三台服务器上的mongod都启动好了后,再分别启动mongos

    1
    numactl --interleave=all mongos -f mongos.conf
  • 注:可以不用numactl –interleave=all 启动,但用numactl关闭NUMA特性,可以提升数据性能

4.3设置第一个分片集

  • 进入服务器1第一个结点

    1
    mongo 172.16.16.1:10001/admin
  • 定义副本集

1
2
3
4
5
6
config = { _id:"shard1", members:[
{_id:0,host:"172.16.16.1:10001",priority:1},
{_id:1,host:"172.16.16.2:10001",priority:2},
{_id:2,host:"172.16.16.3:10001",priority:3}
]
}
  • 初始化副本集
    1
    rs.initiate(config);

4.4设置第二个分片集

  • 进入服务器1第二个结点
1
mongo 172.16.16.1:10002/admin
  • 定义副本集

    1
    2
    3
    4
    5
    6
    config = { _id:"shard2", members:[
    {_id:0,host:"172.16.16.1:10002",priority:2},
    {_id:1,host:"172.16.16.2:10002",priority:3},
    {_id:2,host:"172.16.16.3:10002",priority:1}
    ]
    }
  • 始化副本集

    1
    rs.initiate(config);

4.5设置第三个分片集

  • 进入服务器1第三个结点
1
mongo 172.16.16.1:10003/admin
  • 定义副本集

    1
    2
    3
    4
    5
    6
    config = { _id:"shard3", members:[
    {_id:0,host:"172.16.16.1:10003",priority:3},
    {_id:1,host:"172.16.16.2:10003",priority:1},
    {_id:2,host:"172.16.16.3:10003",priority:2}
    ]
    }
  • 始化副本集

    1
    rs.initiate(config);

4.6设置配置服务器副本集

进入服务器1配置结点

1
mongo 172.16.16.1:20000/admin

  • 定义配置副本集

    1
    2
    3
    4
    5
    6
    config = { _id:"configdb", configsvr: true, members:[
    {_id:0,host:"172.16.16.1:20000",priority:3},
    {_id:1,host:"172.16.16.2:20000",priority:2},
    {_id:2,host:"172.16.16.3:20000",priority:1}
    ]
    }
  • 初始化配置副本集

    1
    rs.initiate(config);

4.7串联路由服务器与分片副本集

  • 进入服务器1路由结点

    1
    mongo 172.16.16.1:30000/admin
  • 设置分片配置

    1
    2
    3
    db.runCommand( { addshard : "shard1/172.16.16.1:10001,172.16.16.2:10001,172.16.16.3:10001"});
    db.runCommand( { addshard : "shard2/172.16.16.1:10002,172.16.16.2:10002,172.16.16.3:10002"});
    db.runCommand( { addshard : "shard3/172.16.16.1:10003,172.16.16.2:10003,172.16.16.3:10003"});
  • 查看分片服务器配置

    1
    db.runCommand({listshards : 1 });

4.8创建管理用户

  • 分别在三台服务器中的primary主结点创建管理员用户

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    use admin;
    db.createUser( { "user" : "root",
    "pwd": "password",
    "roles" : [ { role: "clusterAdmin", db: "admin" },
    { role: "readWriteAnyDatabase", db: "admin" },
    { role: "root", db: "admin" },
    "readWrite"
    ]
    }
    )
  • 生成keyfile(每个进程的key file都保持一致),在服务器中执行以下命令

    1
    2
    3
    cd /var/lib/mongodb/bin/
    openssl rand -base64 753 > keyfile
    chmod 600 keyfile
  • 将生成的keyfile 通过scp拷贝到其他机器的/var/lib/mongodb/bin/目录下

  • 防止scp过程中keyfile文件受损,导致不一至,可通过对比文件的MD5
    1
    md5sum keyfile

4.9重启集群

  • 先停ballancer(部署的时候可以不管,但用于生产过程中,还是先停一下比较保险)

    1
    sh.stopBalancer()
  • 关闭三台服务器的mongod和mongos服务
    可以用 kill 15 pid 来关闭所有mongo

或者

1
2
killall mongos
killall mongod

  • 修改三台服务器/var/lib/mongodb/conf下的配置文件,打开security 的keyfile注释,先启动mongod,再启动mongos
    1
    2
    3
    4
    5
    6
    7
    8
    // 分别启动三台服务器中的结点
    numactl --interleave=all mongod -f shard1.conf
    numactl --interleave=all mongod -f shard2.conf
    numactl --interleave=all mongod -f shard3.conf
    numactl --interleave=all mongod -f configdb.conf
    // 等三台服务器上的mongod都启动好了后,再分别启动mongos
    numactl --interleave=all mongos -f mongos.conf

4.10设置分片键

  • 集群默认是不自动分片,且集群中并非所有数据库和集合都需要采用分片模式,所以我们需要通过手动配置的方式来指定分片数据库和分片集合,其中关键在设置分片键(需要视业务设置),在选定作为分片键的列必须创建索引,所有文档都必须有片键值,(且值不能为null),在集合分片完成后,才可以插入片键值为null的文档;
    如果集合为空,mongodb 将在激活集合分片(sh.shardCollection)时自动创建索引

  • 创建分片键

    1
    2
    3
    4
    mongo 172.16.16.1:30000/admin
    db.auth('root','password')
    db.runCommand({"enablesharding":"IM"});
    db.runCommand({"shardcollection":"IM.record","key":{"appid":1,"to":1}})
  • 15.根据业务需要创建业务账号和密码

    1
    2
    3
    4
    5
    6
    // 创建用户和密码:
    use 数据库名
    db.createUser({ user:"db_user",pwd:"password",roles:[{role:"readWrite", db: "db_name"}]})
    // 访问集群
    mongodb://db_user:password@172.16.16.1:30000,172.16.16.2:30000,172.16.16.3:30000/db_name?readPreference=primaryPreferred

5.完结

JQuery+ajax+jsonp跨域访问

实现过程

客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
<script type="text/javascript" src="resource/js/jquery-1.7.2.js"></script>
</head>
<script type="text/javascript">
$(function(){
/*
//简写形式,效果相同
$.getJSON("http://app.example.com/base/json.do?sid=1494&busiId=101&jsonpCallback=?",
function(data){
$("#showcontent").text("Result:"+data.result)
});
*/
$.ajax({
type : "get",
async:false,
url : "http://app.example.com/base/json.do?sid=1494&busiId=101",
dataType : "jsonp",//数据类型为jsonp
jsonp: "jsonpCallback",//服务端用于接收callback调用的function名的参数
success : function(data){
$("#showcontent").text("Result:"+data.result)
},
error:function(){
alert('fail');
}
});
});
</script>
<body>
<div id="showcontent">Result:</div>
</body>
</html>

服务器端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.sf.json.JSONObject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class ExchangeJsonController {
@RequestMapping("/base/json.do")
public void exchangeJson(HttpServletRequest request,HttpServletResponse response) {
try {
response.setContentType("text/plain");
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expires", 0);
Map<String,String> map = new HashMap<String,String>();
map.put("result", "content");
PrintWriter out = response.getWriter();
JSONObject resultJSON = JSONObject.fromObject(map); //根据需要拼装json
String jsonpCallback = request.getParameter("jsonpCallback");//客户端请求参数
out.println(jsonpCallback+"("+resultJSON.toString(1,1)+")");//返回jsonp格式数据
out.flush();
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

利用iframe和location.hash

实现过程

  • 这个办法比较绕,但是可以解决完全跨域情况下的脚步置换问题。原理是利用location.hash来进行传值。在url: http://a.com#helloword 中的‘#helloworld’就是location.hash,改变hash并不会导致页面刷新,所以可以利用hash值来进行数据传递,当然数据容量是有限的。假设域名a.com下的文件cs1.html要和cnblogs.com域名下的cs2.html传递信息,cs1.html首先创建自动创建一个隐藏的iframe,iframe的src指向cnblogs.com域名下的cs2.html页面,这时的hash值可以做参数传递用。cs2.html响应请求后再将通过修改cs1.html的hash值来传递数据(由于两个页面不在同一个域下IE、Chrome不允许修改parent.location.hash的值,所以要借助于a.com域名下的一个代理iframe;Firefox可以修改)。同时在cs1.html上加一个定时器,隔一段时间来判断location.hash的值有没有变化,一点有变化则获取获取hash值。

A.com下的文件cs1.html文件代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function startRequest(){
var ifr = document.createElement('iframe');
ifr.style.display = 'none';
ifr.src = 'http://www.cnblogs.com/lab/cscript/cs2.html#paramdo';
document.body.appendChild(ifr);
}
function checkHash() {
try {
var data = location.hash ? location.hash.substring(1) : '';
if (console.log) {
console.log('Now the data is '+data);
}
} catch(e) {};
}
setInterval(checkHash, 2000);

Cnblogs.com域名下的cs2.html代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//模拟一个简单的参数处理操作
switch(location.hash){
case '#paramdo':
callBack();
break;
case '#paramset':
//do something……
break;
}
function callBack(){
try {
parent.location.hash = 'somedata';
} catch (e) {
// ie、chrome的安全机制无法修改parent.location.hash,
// 所以要利用一个中间的cnblogs域下的代理iframe
var ifrproxy = document.createElement('iframe');
ifrproxy.style.display = 'none';
ifrproxy.src = 'http://a.com/test/cscript/cs3.html#somedata'; // 注意该文件在"a.com"域下
document.body.appendChild(ifrproxy);
}
}

A.com下的域名cs3.html代码

1
2
//因为parent.parent和自身属于同一个域,所以可以改变其location.hash的值
parent.parent.location.hash = self.location.hash.substring(1);

  • 当然这样做也存在很多缺点,诸如数据直接暴露在了url中,数据容量和类型都有限等……

window.name实现的跨域数据传输

实现过程

有三个页面:

  • a.com/app.html:应用页面。
  • a.com/proxy.html:代理文件,一般是一个没有任何内容的html文件,需要和应用页面在同一域下。
  • b.com/data.html:应用页面需要获取数据的页面,可称为数据页面。
    实现起来基本步骤如下:

  • 在应用页面(a.com/app.html)中创建一个iframe,把其src指向数据页面(b.com/data.html)。
    数据页面会把数据附加到这个iframe的window.name上,data.html代码如下:

1
2
3
4
<script type="text/javascript">
window.name = 'I was there!'; // 这里是要传输的数据,大小一般为2M,IE和firefox下可以大至32M左右
// 数据格式可以自定义,如json、字符串
</script>
  • 在应用页面(a.com/app.html)中监听iframe的onload事件,在此事件中设置这个iframe的src指向本地域的代理文件(代理文件和应用页面在同一域下,所以可以相互通信)。app.html部分代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script type="text/javascript">
var state = 0,
iframe = document.createElement('iframe'),
loadfn = function() {
if (state === 1) {
var data = iframe.contentWindow.name; // 读取数据
alert(data); //弹出'I was there!'
} else if (state === 0) {
state = 1;
iframe.contentWindow.location = "http://a.com/proxy.html"; // 设置的代理文件
}
};
iframe.src = 'http://b.com/data.html';
if (iframe.attachEvent) {
iframe.attachEvent('onload', loadfn);
} else {
iframe.onload = loadfn;
}
document.body.appendChild(iframe);
</script>
  • 获取数据以后销毁这个iframe,释放内存;这也保证了安全(不被其他域frame js访问)。
1
2
3
4
5
<script type="text/javascript">
iframe.contentWindow.document.write('');
iframe.contentWindow.close();
document.body.removeChild(iframe);
</script>
  • 总结起来即:iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。

AJAX跨域访问——XMLHttpRequest代理实现

实现过程

  • 跨域访问简单来说就是A网站的JavaScript代码试图访问B网站,包括提交内容和获取内容.由于安全原因,跨域访问是被各大浏览器所默认禁止的.在广域网环境中,由于浏览器的安全限制,网络连接的跨域访问时不被允许的,XmlHttpRequest也不例外。但有时候跨域访问资源是必需的。

  • 我们不能在浏览器端直接使用AJAX来跨域访问资源,但是在服务器端是没有这种跨域安全限制的。所以,我们只需要让服务器端帮我们完成“跨域访问”的工作,然后在浏览器端用AJAX获取服务器端“跨域访问”的结果就可以了。这就是所谓的在服务器端创建一个 XmlHttpRequest代理,通过这个代理来访问其他域名下的资源。

  • 使用XmlHttpRequest访问同一域名下的资源:直接访问:
    image

  • 用服务器端的XmlHttpRequest代理来跨域访问资源:
    image
  • 前端页面代码

    1
    2
    3
    4
    if(url.indexOf("http://")==0){
    url=url.replace("?","&");
    url="Proxy?url"+url;
    }
  • 后端代码代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    public class Proxy extends javax.servlet.http.HttpServlet {
    protected void doPost(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response)
    throws javax.servlet.ServletException, java.io.IOException {
    response.setContentType("text/html;charset=GB2312");
    String url = request.getParameter("url");
    StringBuffer param = new StringBuffer();
    Enumeration enu = request.getParameterNames();
    int total = 0;
    while(enu.hasMoreElements()){
    String name = (String)enu.nextElement();
    if(!name.equals("url")){
    if(total == 0){
    param.append(name).append("=").append(URLEncoder.encode(request.getParameter(name),"UTF-8"));
    } else{
    param.append("&").append(name).append("=").append(URLEncoder.encode(request.getParameter(name),"UTF-8"));
    }
    total++;
    }
    }
    PrintWriter out = response.getWriter();
    if(url != null){
    URL connect = new URL(url.toString());
    URLConnection connection = connect.openConnection();
    connection.setDoOutput(true);
    OutputStreamWriter paramout = new OutputStreamWriter(connection.getOutputStream());
    paramout.write(param.toString());
    paramout.flush();
    BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(),"GB2312"));
    String line;
    while((line = reader.readLine()) != null){
    out.println(line);
    }
    paramout.close();
    reader.close();
    }
    }

JS创建动态脚本跨域

实现过程

  • script标签本身就可以访问其它域的资源,不受浏览器同源策略的限制,可以通过在页面动态创建script标签,代码如下:
1
2
3
var script = document.createElement('script');
script.src = "http://aa.xx.com/js/*.js";
document.body.appendChild(script);
  • 这样通过动态创建script标签就可以加载其它域的js文件,然后通过本页面就可以调用加载后js文件的函数,这样做的缺陷就是不能加载其它域的文档,只能是js文件,jsonp便是通过这种方式实现的,jsonp通过向其它域传入一个callback参数,通过其他域的后台将callback参数值和json串包装成javascript函数返回,因为是通过script标签发出的请求,浏览器会将返回来的字符串按照javascript进行解析执行,实现了域与域之间的数据传输。

通过修复web.xml来实现跨域

实现过程

  • 在开发项目中加入支持库,或把支持库直接丢到tomcat的lib目录下,下载地址:http://cdn.besdlab.cn/cors-lib.rar
  • 修改web.xml,增加以下代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<filter>
<filter-name>CORS</filter-name>
<filter-class>com.thetransactioncompany.cors.CORSFilter</filter-class>
<init-param>
<param-name>cors.allowOrigin</param-name>
<param-value>*</param-value>
</init-param>
<init-param>
<param-name>cors.supportedMethods</param-name>
<param-value>GET, POST, HEAD, PUT, DELETE</param-value>
</init-param>
<init-param>
<param-name>cors.supportedHeaders</param-name>
<param-value>Accept, Origin, X-Requested-With, Content-Type, Last-Modified</param-value>
</init-param>
<init-param>
<param-name>cors.exposedHeaders</param-name>
<param-value>Set-Cookie</param-value>
</init-param>
<init-param>
<param-name>cors.supportsCredentials</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CORS</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

通过nginx实现跨域请求

实现过程

  • 把下面的配置保存成一个文件,例如:nginx_cors,引入到Nginx的代理配置中去。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
#
# Custom headers and headers various browsers *should* be OK with but aren't
#
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
#
# Tell client that this pre-flight info is valid for 20 days
#
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 0;
return 204;
}
if ($request_method = 'POST') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
}
if ($request_method = 'GET') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
}
1
2
3
4
5
6
7
8
9
10
11
location ~* ^/ {
proxy_pass http://www.jd.com;
access_log /var/log/nginx/jd-access.log main;
include /etc/nginx/nginx_cors;
max_ranges 0;
}
  • 再通过访问Nginx这台机器上的地址,就能跨域访问www.jd.com下所有非权限控制的资源和数据了。

我的新博客上线啦

欢迎来到我的博客

  • 工欲善其事,必先利其器,成长离不开积累和历练,千里之行始于足下,如今我就开始好好的做好自己,写博客不是因为有人需要我,而是我很需要借此机会成长!