Nginx tcp负载均衡模块:
1.将client的请求按照 负载均衡算法 分发到服务器
2.负载均衡器与服务器保持心跳机制,监测故障、保障服务可靠性
3.可以发现添加新的服务器,方便扩展服务器集群的数量
Nginx反向代理用途:
2.4 用途
- 隐藏服务器真实ip:使用反向代理,可以对客户端隐藏服务器的ip地址
- 负载均衡:根据所有真实服务器的负载情况,将客户端请求分发到不同的服务器上
- 提高访问速度:反向代理服务器可以对静态内容及短时间内有大量访问请求的动态内容提供缓存服务,提高访问速度
- 提供安全保障:反向代理服务器可以作为应用层防火墙,为网站提供对基于web的攻击行为(例如DoS/DDoS)的防护,更容易排查恶意软件等。还可以为后端服务器统一提供加密和SSL加速(如SSL终端代理),提供HTTP访问认证等。
Nginx 的配置:
Nginx 的软件包中,我们需要在conf目录下的 nginx.conf文件进行应用配置
#nginx tcp loadbalance config stream{ upstream MyServer{ server 127.0.0.1:6000 weight=1 max_fails=3 fail_timeout=30s; server 127.0.0.1:6002 weight=1 max_fails=3 fail_timeout=30s; #服务器配置:地址:端口号 权重 最多失败次数 每次超时事件,超过视为失败(>实现了心跳机制,保障服务可靠性) #若扩展新的服务器,则在此处继续添加即可 } server{ proxy_connect_timeout 1s;#若第一次握手时间超过1s,视为失败 #proxy_timeout 3s;#连接3s后自动断开(短连接),该服务器聊天需要长连接所以将此注释 listen 8000;#监听8000端口,Nginx的反向代理监听端口(客户端直接连接的端口 proxy_pass MyServer;#标记需要代理的服务器集群 tcp_nodelay on;
基于weight 的权重配置,当所有权重均一致时,实行轮询分发给各个服务器
由服务器的需求和硬件限制时,权重随其相应变化,达到最佳性能
配置完成后:
nginx -s reload //重新加载配置文件启动
./nginx -s reload //平滑重启
Nginx默认安装到 /usr/local/下的nginx目录,进入nginx/sbin内,以管理员身份运行可执行文件
可以通过netstat -tanp 查看nginx的运行情况,nginx服务器为http服务器,为80端口
跨平台通信流程:(Redis 观察者模式)
- client1分配在ChatServer1后,ChatServer1 在Redis消息队列中subscribe:订阅client1的信息,client2分配在ChatServer2后,ChatServer2 在Redis消息队列中subscribe:订阅client2的信息
- client1 在Charserver1 上登录后,需要给好友client2发送信息,而好友由负载均衡算法分配到Chat Server2上,在ChatServer1的用户登录map表内没有client2 的信息,继而查询用户数据库client2的数据登录显示好友已经登陆
- 将需发送的信息publish chat _json:发布到Redis消息队列中,Redis收到后将信息notify:提示给ChatServer2(订阅client2的信息)实现跨服务器通信,而ChatServer2 从Redis 消息队列中获取client1 的信息
从而实现跨服务器的通信(Redis:)
subscribe +s :订阅序号为s的信息后阻塞监听状态,仅接收订阅的信息
publish +s :发布关于序号s的信息,发布成功后订阅方即可接收
Redis 配置文件
.h文件
#ifndef REDIS_H #define REDIS_H #include <hiredis/hiredis.h> #include <thread> #include <functional> using namespace std; class Redis { public: Redis(); ~Redis(); // 连接redis服务器 bool connect(); // 向redis指定的通道channel发布消息 bool publish(int channel, string message); // 向redis指定的通道subscribe订阅消息 bool subscribe(int channel); // 向redis指定的通道unsubscribe取消订阅消息 bool unsubscribe(int channel); // 在独立线程中接收订阅通道中的消息 void observer_channel_message(); // 初始化向业务层上报通道消息的回调对象 void init_notify_handler(function<void(int, string)> fn); private: // hiredis同步上下文对象,负责publish消息 redisContext *_publish_context; // hiredis同步上下文对象,负责subscribe消息 redisContext *_subcribe_context; // 回调操作,收到订阅的消息,给service层上报 function<void(int, string)> _notify_message_handler; }; #endif
Redis.cpp
#include "redis.hpp" #include <iostream> using namespace std; Redis::Redis() : _publish_context(nullptr), _subcribe_context(nullptr) { } Redis::~Redis() { if (_publish_context != nullptr) { redisFree(_publish_context); } if (_subcribe_context != nullptr) { redisFree(_subcribe_context); } } bool Redis::connect() { // 负责publish发布消息的上下文连接 _publish_context = redisConnect("127.0.0.1", 6379); if (nullptr == _publish_context) { cerr << "connect redis failed!" << endl; return false; } // 负责subscribe订阅消息的上下文连接 _subcribe_context = redisConnect("127.0.0.1", 6379); if (nullptr == _subcribe_context) { cerr << "connect redis failed!" << endl; return false; } // 在单独的线程中,监听通道上的事件,有消息给业务层进行上报 thread t([&]() { observer_channel_message(); }); t.detach(); cout << "connect redis-server success!" << endl; return true; } // 向redis指定的通道channel发布消息 bool Redis::publish(int channel, string message) { redisReply *reply = (redisReply *)redisCommand(_publish_context, "PUBLISH %d %s", channel, message.c_str()); if (nullptr == reply) { cerr << "publish command failed!" << endl; return false; } freeReplyObject(reply); return true; } // 向redis指定的通道subscribe订阅消息 bool Redis::subscribe(int channel) { // SUBSCRIBE命令本身会造成线程阻塞等待通道里面发生消息,这里只做订阅通道,不接收通道消息 // 通道消息的接收专门在observer_channel_message函数中的独立线程中进行 // 只负责发送命令,不阻塞接收redis server响应消息,否则和notifyMsg线程抢占响应资源 if (REDIS_ERR == redisAppendCommand(this->_subcribe_context, "SUBSCRIBE %d", channel)) { cerr << "subscribe command failed!" << endl; return false; } // redisBufferWrite可以循环发送缓冲区,直到缓冲区数据发送完毕(done被置为1) int done = 0; while (!done) { if (REDIS_ERR == redisBufferWrite(this->_subcribe_context, &done)) { cerr << "subscribe command failed!" << endl; return false; } } // redisGetReply return true; } // 向redis指定的通道unsubscribe取消订阅消息 bool Redis::unsubscribe(int channel) { if (REDIS_ERR == redisAppendCommand(this->_subcribe_context, "UNSUBSCRIBE %d", channel)) { cerr << "unsubscribe command failed!" << endl; return false; } // redisBufferWrite可以循环发送缓冲区,直到缓冲区数据发送完毕(done被置为1) int done = 0; while (!done) { if (REDIS_ERR == redisBufferWrite(this->_subcribe_context, &done)) { cerr << "unsubscribe command failed!" << endl; return false; } } return true; } // 在独立线程中接收订阅通道中的消息 void Redis::observer_channel_message() { redisReply *reply = nullptr; while (REDIS_OK == redisGetReply(this->_subcribe_context, (void **)&reply)) { // 订阅收到的消息是一个带三元素的数组 if (reply != nullptr && reply->element[2] != nullptr && reply->element[2]->str != nullptr) { // 给业务层上报通道上发生的消息 _notify_message_handler(atoi(reply->element[1]->str) , reply->element[2]->str); } freeReplyObject(reply); } cerr << ">>>>>>>>>>>>> observer_channel_message quit <<<<<<<<<<<<<" << endl; } void Redis::init_notify_handler(function<void(int,string)> fn) { this->_notify_message_handler = fn; }
Redis 问题:
问题背景:在集群服务器中采用Redis发布订阅功能作为消息中间件,解耦合服务器的消息通信,实现跨服务器之间的通信,以hiredis 作为客户端编程
1. 问题一:Publish 向中间件发布信息不成功
?hiredis提供发布消息的接口函数redisCommend 在发布-订阅命令需要在不同的上下文环境中执行
redisReply *reply = (redisReply *)redisCommand(_publish_context, "PUBLISH %d %s", channel, message.c_str());
_publish_context(发布的上下文环境)
_subscribe_conntext(订阅的上下文环境)
2. 问题2:subscribe 订阅失败,客户端无响应
查看出现问题的服务器线程号
stu@stu-VMware-Virtual-Platform:~/obj_chat$ ps -ef | grep Server stu 5904 1529 0 11:08 pts/1 00:00:00 ./Server.out 127.0.0.1 9000 stu 5909 1442 0 11:08 pts/0 00:00:00 ./Server.out 127.0.0.1 9002 stu 6004 5991 0 11:13 pts/4 00:00:00 grep --color=auto Server
在登录端口号9002的服务器 进程号为5909的服务器出现问题,通过gdb调试:用info threads可以输出当前进程所有线程的信息,可以看到:
Server.out是主线程,也就是muduo库的I/O线程,现在处理epoll_wait状态,等待新用户的连接;
- 而EventLoop事件循环有三个线程,分别是ChatServer0、ChatServer1、ChatServer2,
- 其中ChatServer1和ChatServer2处在epoll_wait状态,等待已连接用户的读写事件,
- 但是ChatServer0却阻塞在__libc_recv函数处,不能继续处理逻辑业务,不能给客户端回复响应,导致客户端无应答。
线程池里面的redisGetReply抢了上面订阅subscribe的redisCommand底层调用的redisGetReply的响应消息,导致ChatServer0线程阻塞在这个接口调用上,无法再次回到epoll_wait处了,这个线程就废掉了,如果工作线程全部发生这种情况,最终服务器所有的工作线程就全部停止工作了!
解决方案
从hiredis的redisCommand源码上可以看出,它实际上相当于调用了这三个函数:
- redisAppendCommand 把命令写入本地发送缓冲区
- redisBufferWrite 把本地缓冲区的命令通过网络发送出去
- redisGetReply 阻塞等待redis server响应消息
既然在muduo库的ThreadPool中单独开辟了一个线程池,接收this->_context上下文的响应消息,因此subcribe订阅消息只做消息发送,不做消息接收就可以了,如下:
// /订阅通道 void subscribe(int channel) { // 只负责发送命令,不阻塞接收redis server响应消息,否则和notifyMsg线程抢占响应资源 if (REDIS_ERR == redisAppendCommand(this->_context, "SUBSCRIBE %d", channel)) { LOG_ERROR << "subscribe [" << channel << "] error!"; return; } // redisBufferWrite可以循环发送缓冲区,直到缓冲区数据发送完毕(done被置为1) int done = 0; while (!done) { if (REDIS_ERR == redisBufferWrite(this->_context, &done)) { LOG_ERROR << "subscribe [" << channel << "] error!"; return; } } LOG_INFO << "subscribe [" << channel << "] success!";
3. Redis 的动态库调用问题:
编译器只会使用/lib和/usr/lib这两个目录下的库文件,通常通过源码包进行安装时,如果不指定--prefix,会将库安装在/usr/local/lib目录下;当运行程序需要链接动态库时,提示找不到相关的.so库,会报错。也就是说,/usr/local/lib目录不在系统默认的库搜索目录中,需要将目录加进去。
1、首先打开/etc/ld.so.conf文件:sudo vi /etc/ld.so.conf
2、加入动态库文件所在的目录:执行vi /etc/ld.so.conf,在"include ld.so.conf.d/*.conf"下方增加"/usr/local/lib"。
3. 运行一下ldconfig,使所有的库文件都被缓存到文件/etc/ld.so.cache中,如果没做,可能会找不到刚安装的库。sudo ldconfig
一定要执行ldconfig。否则可能目录下已经有.so文件也可能会报找不到的错误。