Flask在Windows环境下的部署

背景由于目前在用的Flask项目涉及到一部分依赖Windows的处理,还无法迁移到linux平台,那么在windows环境下,要怎么部署呢?思路根据Flask官网介绍,由于Flask内置的服务器性能不佳,推荐的主要的部署方式有如下几种:mod_wsgi (Apache)独立 WSGI 容器GunicornTornadoGeventuWSGIFastCGICGI上述这些部署方式,仅Tornado是支持在windows情况下部署的,配合上Nginx可以达到比较好的效果。可已参考Nginx与tornado框架的并发评测。但是在实际使用中发现,tornado 的稳定性虽然很高,但是在tornado上部署Flask,并不会有异步的效果。实际上还是单进程阻塞运行的,即使在Flask中配置了threaded = True也无法实现多线程使用。Flask多线程情况配置启用多线程:# manage.pyfrom flask_script import Serverserver = Server(host=“0.0.0.0”, threaded=True)在Flask中配置两条测试路由import time@main.route(’/test’)def maintest(): return ‘hello world’ @main.route(’/sleep’)def mainsleep(): time.sleep(60) return ‘wake up’先用浏览器访问\sleep:随即立刻访问\test:可见两次访问是不同的线程处理的,不会出现堵塞的情况。tornado + Flask多线程情况使用tornado托管:from tornado.wsgi import WSGIContainerfrom tornado.httpserver import HTTPServerfrom tornado.ioloop import IOLoopfrom yourapplication import apphttp_server = HTTPServer(WSGIContainer(app))http_server.listen(5000)IOLoop.instance().start()先用浏览器访问\sleep:随即立刻访问\test:可以发现,虽然tornado框架是支持异步的,但是由于实际上后台的处理是同步的,从而无法实现异步的处理的效果。如果想后台的处理也异步,则需要直接使用tornado来开发。那么为什么使用tornado来托管flask呢?Tornado 是一个开源的可伸缩的、非阻塞式的 web 服务器和工具集,它驱动了FriendFeed 。因为它使用了 epoll 模型且是非阻塞的,它可以处理数以千计的并发固定连接,这意味着它对实时 web 服务是理想的。把 Flask 集成这个服务是直截了当的根据官网描述,其实也是为了弥足flask自带服务器不稳定的问题。Flask高并发下的表现使用tsung进行压测,压力500:Namehighest 10sec meanlowest 10sec meanHighest RateMean RateMeanCountconnect34.30 msec31.91 msec506 / sec356.60 / sec33.19 msec103908page0.42 sec0.29 sec505 / sec356.32 / sec0.39 sec103782request0.42 sec0.29 sec505 / sec356.32 / sec0.39 sec103782session1mn 24sec10.64 sec11.4 / sec1.21 / sec14.24 sec362CodeHighest RateMean RateTotal number200505 / sec356.32 / sec104792NameHighest RateTotal numbererror_abort0.5 / sec1error_abort_max_conn_retries11.7 / sec362error_connect_econnrefused58.6 / sec1667可见,在500的并发下,效果不佳,有很多的链接拒绝。Flask + Nginx在高并发下的表现使用tsung进行压测,压力500:Namehighest 10sec meanlowest 10sec meanHighest RateMean RateMeanCountconnect0.20 sec30.95 msec1810.5 / sec626.43 / sec0.11 sec189853page0.68 sec0.17 sec1810.1 / sec625.72 / sec0.40 sec189581request0.68 sec0.17 sec1810.1 / sec625.72 / sec0.40 sec189581CodeHighest RateMean RateTotal number200906.4 / sec196.08 / sec606895021443.9 / sec430.02 / sec129006NameHighest RateTotal numbererror_abort0.5 / sec1情况差不多,Flask服务器表现还算稳定,那么尝试增加后台Flask服务器数量(通过多端口实现):python manage.py runserver –port=8001python manage.py runserver –port=8002python manage.py runserver –port=8003python manage.py runserver –port=8004使用tsung进行压测,压力500,4个Flask服务器:Namehighest 10sec meanlowest 10sec meanHighest RateMean RateMeanCountconnect0.18 sec32.57 msec3510.1 / sec639.92 / sec0.11 sec195154page0.49 sec85.30 msec3512.1 / sec639.07 / sec0.35 sec194856request0.49 sec85.30 msec3512.1 / sec639.07 / sec0.35 sec194856CodeHighest RateMean RateTotal number2003510.1 / sec639.50 / sec194986NameHighest RateTotal numbererror_abort0.333333333333333 / sec1这个效果妥妥的。使用tsung进行压测,压力1000,4个Flask服务器:Namehighest 10sec meanlowest 10sec meanHighest RateMean RateMeanCountconnect0.20 sec32.63 msec2983.8 / sec492.94 / sec98.56 msec150793page0.57 sec90.00 msec2976.4 / sec491.31 / sec0.40 sec150275request0.57 sec90.00 msec2976.4 / sec491.31 / sec0.40 sec150275CodeHighest RateMean RateTotal number2002981.4 / sec488.92 / sec14955650292.5 / sec4.02 / sec925NameHighest RateTotal numbererror_abort0.333333333333333 / sec1开始有一些502的超时错误了。使用tsung进行压测,压力1000,4个tornado服务器:Namehighest 10sec meanlowest 10sec meanHighest RateMean RateMeanCountconnect0.18 sec86.24 msec2052.1 / sec693.82 / sec0.14 sec208786page0.52 sec0.24 sec2060.7 / sec693.34 / sec0.45 sec208606request0.52 sec0.24 sec2060.7 / sec693.34 / sec0.45 sec208606CodeHighest RateMean RateTotal number2002056.6 / sec693.67 / sec208703在并发1000的情况下,是否使用tornado托管Flask效果差不多。结论根据上述测试,直接使用Flask服务器的话,由于并发处理较弱,会有各种超时或者连接拒绝的错误。通过搭配Nginx来进行缓冲,通过增加后端服务器数来提供并发处理量。所以最终选择了Nginx+后台4个Flask服务器的方式。由于目前Flask项目全体用户只有几千,目前并发情况很低,该方式完全满足使用。如果在更大型项目中,并发上万,建议还是考虑想办法迁移至Liunx环境,通过官方建议的方式部署。 ...

February 11, 2019 · 2 min · jiezi

Nginx + Node + Vue 部署初试

趁着爸妈做年夜饭之前,把之前做的笔记贴出来,新的一年到了,祝大家Nginx + Node + Vue 部署初试知乎个人博客Github日常学习笔记Nginx定义异步框架的 Web服务器,也可以用作反向代理,负载平衡器 , HTTP缓存, 媒体流等的开源软件。它最初是一个旨在实现最高性能和稳定性的Web服务器。除了HTTP服务器功能外,NGINX还可以用作电子邮件(IMAP,POP3和SMTP)的代理服务器以及HTTP,TCP和UDP服务器的反向代理和负载平衡器。特点更快高扩展性, Nginx的模块都是嵌入到二进制文件中执行高可靠性低内存消耗单机支持10万次的并发连接热部署, master管理进行与work工作进程分离设计,因此具备热部署功能最自由的BSD许可协议功能静态服务(css , js , html, images, videos)SSL 和 TLS SNI 支持HTTP/HTTPS, SMTP, IMAP/POP3 反向代理FastCGI反向代理负载均衡页面缓存(CDN)支持gzip、expirse支持 keep-alive 和管道连接基于 PCRE 的 rewrite 重写模块带宽限制基于IP 和名称的虚拟主机服务支持访问速率、并发限制反向代理(适用2000WPV、并发连接1W/秒),简单的负载均衡和容错基于客户端 IP 地址和 HTTP 基本认证的访问控制Mac 安装Nginx// 推荐使用brew, 安装homebrew/usr/bin/ruby -e “$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"// Homebrew 安装 Nginx brew install nginx// Mac 下 Nginx的目录cd /usr/local/etc/nginxll -alvim nginx.confhomebrew详见Nginx 参数列表配置参数属性解释说明参数列表user设置nginx服务的系统使用用户nobody(注意:此处用户如果比启动Nginx的用户权限低,你需要使用当前用户重启Nginx)nginx -s stop 关闭nginx-> nginx 启动-> ps auxgrep nginx查看启动用户worker_processes开启的线程数一般与服务器核数保持一致error_log定位全局错误日志文件错误日志定义等级,[ debug \info notice \warn \error \crit ],debug输出最多,crir输出最少pid指定进程id的存储文件位置 worker_rlimit_nofile指定一个nginx进程打开的最多文件描述符数目,受系统进程的最大打开文件数量限制 events包含Nginx中所有处理连接的设置 httpNginx http处理的所有核心特性 Event配置参数属性解释说明参数列表worker_connections定义每个进程的最大连接数,受系统进程的最大打开文件数量限制单个后台worker process进程的最大并发链接数 (最大连接数= worker_processes worker_connections)在反向代理环境下:<br/>最大连接数 = worker_processes worker_connections / 4use工作进程数[ epoll /dev/poll \poll \eventport \kqueue \select \rtsig ]multi_accept一个新连接通知后接受尽可能多的连接on / offaccept_mutex开启或者禁用使用互斥锁来打开socketson / offEvent Use支持的事件模型Events详见HTTP配置参数属性解释说明参数列表include主模块指令,实现对配置文件所包含的文件的设定,可以减少主配置文件的复杂度,DNS主配置文件中的zonerfc1912,acl基本上都是用include语句 default_type核心模块指令默认设置为二进制流,也就是当文件类型未定义时使用这种方式log_format日志格式的设定日志格式的名称,可自行设置,后面引用access_log引用日志引用log_format设置的名称keepalive_timeout设置客户端连接保存活动的超时时间0是无限制sendfile开启高效文件传输模式on / offtcp_nopush开启防止网络阻塞on / offtcp_nodelay开启防止网络阻塞on / offupstream负载均衡 serverNginx的server虚拟主机配置 Upstream配置参数属性解释说明轮询(默认)每个请求按访问ip的hash结果分配,这样每个访客固定访问一个后端服务器,可以解决session的问题。weight指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况ip_hash每个请求按访问ip的hash结果分配,这样每个访客固定访问一个后端服务器,可以解决session的问题。fair(第三方)按后端服务器的响应时间来分配请求,响应时间短的优先分配。url_hash(第三方)按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,后端服务器为缓存时比较有效。weight 默认为1.weight越大,负载的权重就越大。Nginx Upstream 状态例如:upstream news { server 127.0.0.1:9527 backup; server 127.0.0.1:9527 weight=1 max_fails=2 fail_timeout=3; …}配置参数属性解释说明backup预留的备份服务器down当前的server暂时不参与负载均衡fail_timeout经过max_fails 失败后,服务暂停的时间max_conns限制最大的接收的连接数max_fails允许请求失败的次数use location:在server中添加proxy_pass http://127.0.0.1:9527;// 因为我的API接口是这个,such as /api/profile// location 具体匹配规则详见后面location ~ /api/ { proxy_pass http://127.0.0.1:9527;}Server配置参数属性解释说明参数列表listen监听端口http -> 80 / https -> 443server_name设置主机域名localhostcharset设置访问的语言编码 access_log设置虚拟主机访问日志的存放路径及日志的格式 location设置虚拟主机的基本信息 Location配置参数属性解释说明参数列表root设置虚拟主机的网站根目录Vue项目的根目录/Users/rainy/Desktop/MyWork/Work/cloudwiz-website/distindex设置虚拟主机默认访问的网页index.html index.htmproxy通过不同协议将请求从NGINX传递到代理服务器 =: 开头表示精确匹配,如 api 中只匹配根目录结尾的请求,后面不能带任何字符串.^~ :开头表示uri以某个常规字符串开头,不是正则匹配.: 开头表示区分大小写的正则匹配.: 开头表示不区分大小写的正则匹配./ : 通用匹配, 如果没有其它匹配,任何请求都会匹配到.匹配优先级(高到低)location =location 完整路径location ^~ 路径location , 正则顺序location 部分起始路径/详见Location配置Reverse Proxy当NGINX代理请求时,它会将请求发送到指定的代理服务器,获取响应并将其发送回客户端。可以使用指定的协议将请求代理到HTTP服务器(另一个NGINX服务器或任何其他服务器)或非HTTP服务器(可以运行使用特定框架(如PHP或Python)开发的应用程序)。location / some / path / { proxy_pass http://www.example.com:8080; proxy_set_header Host $ host ; proxy_set_header X-Real-IP $ remote_addr ; // 禁用特定位置的缓冲 proxy_buffering off ; proxy_buffers 16 4k ; proxy_buffer_size 2k ; proxy_bind 127.0.0.2 ; // IP地址也可以用变量指定}将请求传递给非HTTP代理服务器,**_pass应使用相应的指令:fastcgi_pass 将请求传递给FastCGI服务器uwsgi_pass 将请求传递给uwsgi服务器scgi_pass 将请求传递给SCGI服务器memcached_pass 将请求传递给memcached服务器配置参数属性解释说明参数列proxy_pass将请求传递给HTTP代理服务器 proxy_set_header传递请求标头默认情况下,NGINX在代理请求中重新定义两个头字段“Host”和“Connection”,并删除其值为空字符串的头字段。“Host”设置为$proxy_host变量,“Connection”设置为close。proxy_buffering负责启用和禁用缓冲on / offproxy_buffers请求分配的缓冲区的大小和数量 proxy_buffer_size代理服务器的响应的第一部分存储在单独的缓冲区大小通常包含一个相对较小的响应头,并且可以比其余响应的缓冲区小。proxy_bind接受来自特定IP网络或IP地址范围的连接指定proxy_bind必要网络接口的指令和IP地址详见Proxy全局变量Global Variable变量名变量含义$args请求中的参数$content_lengthHTTP请求信息里的Content-Length$content_type请求信息里的Content-Type$host请求信息中的Host,如果请求中没有Host行,则等于设置的服务器名$http_cookiecookie 信息$http_referer引用地址$http_user_agent客户端代理信息$remote_addr客户端地址$remote_port客户端端口号$remote_user客户端用户名,认证用$request_method请求的方法,比如GET、POST等$request_uri完整的原始请求URI(带参数)$scheme请求方案,http或https$server_addr接受请求的服务器的地址,如果没有用listen指明服务器地址,使用这个变量将发起一次系统调用以取得地址(造成资源浪费);$server_protocol请求的协议版本,HTTP/1.0或HTTP/1.1$uri请求中的当前URI, $uri在请求处理期间 ,值可能会发生变化,例如在执行内部重定向或使用索引文件时全局变量详见Alphabetical index of variables修改 http server中的配置启动Nginxnginxps -ef | grep nginx重启Nginxnginx -s reload关闭Nginxnginx -s stop因为我已经启动了,所以就重启一下Nginx即可Linux安装NginxLinux安装常见的Linux命令使用pstree查看当前服务器启动的进程pstree查找Nginx的位置ps -aux | grep nginx进入nginx的目录然后配置nginx.conf文件即可Docker安装Nginx查找 Docker Hub 上的 nginx镜像docker search nginx拉取官方镜像docker pull nginx查看当前镜像docker images nginxServer Treetree -C -L 3 -I ‘node_modules’├── server│ ├── app.js│ ├── db│ │ ├── db.js│ │ └── newsSql.js│ ├── package-lock.json│ ├── package.json│ └── routers│ ├── news.js│ └── router.jsNode Servernpm initnpm install express mysql body-parser -Sapp.jsconst express = require(’express’)const bodyParser = require(‘body-parser’)const app = express()const router = require(’./routers/router’)const PORT = 9527app.use(bodyParser.json())app.use(bodyParser.urlencoded({ extended: true }))app.use(router)app.listen(PORT, () => { console.log(Listen port at ${PORT})})db.js -> Mysql配置module.exports = { mysql: { host: ’localhost’, user: ‘root’, password: ‘xxxx’, database: ’test’, port: ‘3306’ }}router.jsconst express = require(’express’)const router = express.Router()const news = require(’./news’)router.get(’/api/news/queryAll’, (req, res, next) => { news.queryAll(req, res, next)})router.get(’/api/news/query’, (req, res, next) => { news.queryById(req, res, next)})router.post(’/api/news/add’, (req, res, next) => { news.add(req, res, next)})router.post(’/api/news/update’, (req, res, next) => { news.update(req, res, next)})router.delete(’/api/news/deleteNews’, (req, res, next) => { news.deleteNews(req, res, next)})module.exports = routernewSql.jsmodule.exports = { createNews: CREATE TABLE news ( id int(255) NOT NULL AUTO_INCREMENT, type varchar(255) CHARACTER SET utf8 NOT NULL, title varchar(255) CHARACTER SET utf8 NOT NULL, description varchar(255) CHARACTER SET utf8 NOT NULL, occur_time varchar(255) CHARACTER SET utf8 NOT NULL, url varchar(255) NOT NULL, newsImg varchar(255) NOT NULL, PRIMARY KEY (id) ), queryAll: ‘SELECT * FROM news’, queryById: ‘SELECT * FROM news WHERE id = ?’, add: ‘INSERT INTO news (type, title, description, occur_time, url, newsImg) VALUES (?, ?, ?, ?, ?, ?)’, update: ‘UPDATE news SET type = ?, title = ?, description = ?, occur_time = ?, url = ?, newsImg = ? WHERE id = ?’, delete: ‘DELETE FROM news WHERE id = ?’}news.jsconst mysql = require(‘mysql’)const db = require(’../db/db’)const $newsSql = require(’../db/newsSql’)let pool = mysql.createPool(db.mysql)let queryAll = (req, res, next) => { pool.getConnection((error, connect) => { if (error) { throw error } else { connect.query($newsSql.queryAll, (error, data) => { if (error) { throw error } res.json({ code: ‘200’, msg: ‘success’, data }) connect.release() }) } })}let queryById = (req, res, next) => { let id = +req.query.id pool.getConnection((error, connect) => { if (error) { throw error } else { connect.query($newsSql.queryById, id, (error, data) => { if (error) { throw error } res.json({ code: ‘200’, msg: ‘success’, data }) connect.release() }) } })}let add = (req, res, next) => { let rb = req.body let params = [rb.type, rb.title, rb.description, rb.occur_time, rb.url, rb.newsImg] pool.getConnection((error, connect) => { if (error) { throw error } else { connect.query($newsSql.add, params, (error, data) => { if (error) { throw error } res.json({ code: ‘200’, msg: ‘success’ }) connect.release() }) } })}let update = (req, res, next) => { let rb = req.body let params = [rb.type, rb.title, rb.description, rb.occur_time, rb.url, rb.newsImg, rb.id] pool.getConnection((error, connect) => { if (error) { throw error } else { connect.query($newsSql.update, […params], (error, data) => { if (error) { throw error } res.json({ code: ‘200’, msg: ‘success’ }) connect.release() }) } })}let deleteNews = (req, res, next) => { let id = +req.query.id pool.getConnection((error, connect) => { if (error) { throw error } else { connect.query($newsSql.delete, id, (error, data) => { if (error) { throw error } res.json({ code: ‘200’, msg: ‘success’ }) connect.release() }) } })}module.exports = { queryAll, queryById, add, update, deleteNews}Vue配置代理以及使用apiconfig/index.js 修改proxyTablemodule.exports = { dev: { proxyTable: { ‘/api’: { target: ‘http://127.0.0.1:9527’, changeOrigin: true, pathRewrite: { ‘^/api’: ‘/api’ } } } }}使用axios调用接口import axios from ‘axios’export default { created () { this._getAllNews() }, methods: { _getAllNews () { axios.get(’/api/news/queryAll’).then(res => { if (+res.data.code === SCC_CODE) { this.news = res.data.data } }) } }}Vue build打包npm run build因为我的Server端是Express写的,启动Server需要使用Node,所以我们需要在服务器上安装Node.Linux安装Node 8.x# Using Ubuntucurl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash -sudo apt-get install -y nodejs# Using Debian, as rootcurl -sL https://deb.nodesource.com/setup_8.x | bash -apt-get install -y nodejs# Using Centoscurl -sL https://rpm.nodesource.com/setup_8.x | bash -yum install -y nodejs具体安装各版本的Node详见启动Node此处我之前的命令执行错误,所以我需要kill这个进程nohup node website/server/app.js &nohup:可以将程序以忽略挂起信号的方式运行起来,被运行的程序的输出信息将不会显示到终端。无论是否将 nohup 命令的输出重定向到终端,输出都将附加到当前目录的 nohup.out文件中。如果当前目录的 nohup.out 文件不可写,输出重定向到$HOME/nohup.out文件中。如果没有文件能创建或打开以用于追加,那么 command 参数指定的命令不可调用。如果标准错误是一个终端,那么把指定的命令写给标准错误的所有输出作为标准输出重定向到相同的文件描述符。到这里,我们的Web,Node Server ,Nginx都已经配置并启动好了,我们只需要到浏览器输入你的服务器IP:8080即可.Nginx众多概念详见官方词汇表什么是应用交付?什么是应用交付控制器(ADC)?什么是应用程序服务器与Web服务器?什么是缓存?什么是云负载平衡?什么是聚类?什么是DevOps?什么是分布式拒绝服务(DDoS)?什么是DNS负载平衡?什么是全局服务器负载平衡?什么是高可用性?什么是HTTP?什么是HTTP / 2?什么是混合负载均衡?什么是第4层负载均衡?什么是第7层负载平衡?什么是负载平衡?什么是媒体服务器?什么是微服务?什么是网络负载均衡器?什么是NGINX?什么是渐进式下载?什么是反向代理服务器?什么是反向代理与负载均衡器?什么是循环负载平衡?什么是面向服务的体系结构(SOA)?什么是会话持久性?什么是SSL负载均衡器?什么是Web加速?什么是Web服务器? ...

February 4, 2019 · 4 min · jiezi

Traefik 入手及简单配置

Traefik 入手及简单配置Traefik 与 nginx 一样,是一款反向代理的工具,至于使用他原因基于以下几点漂亮的 dashboard 界面可基于容器 label 进行配置新添服务简单,不用像 nginx 一样复杂配置,并且不用频繁重启对 prometheus 和 k8s 的集成尝试一下…接下来讲一下它的基本功能以及文件配置安装下载二进制文件,指定配置文件,直接执行可以启动。./traefik -c traefik.toml当然,你也可以通过 docker 启动,参考 Traefik Get Started。另外,如果需要使用 docker 启动,需要所有的服务都在一个 network 中,或者设置 traefik 的 network 为 host。启动成功后,可以访问 localhost:8080 访问 Dashboard 页面。问题每当有配置改动需要重启时,只能杀了进程,然后启动,导致服务有短暂的暂停补充一下,每当使用 docker 新添一个服务时,不需要更改配置,或者更改部分配置时,如 file provider,traefik 会监听配置文件变化并自动重启。但是需要修改 https 的证书或者日志的路径时,需要手动重启。所以需要手动重启的时候并不是很多。当配置文件修改后,会有语法错误,无法向 nginx -t 一样检查是否配置文件有问题日志[accessLog]# Sets the file path for the access log. If not specified, stdout will be used.# Intermediate directories are created if necessary.## Optional# Default: os.Stdout#filePath = “./traefik-access.json”# Format is either “json” or “common”.## Optional# Default: “common”#format = “json"日志文件配置为 json 格式,方便调试。同时,强烈推荐 jq,一款 linux 下解析 json 的工具。以下是两个常用的命令,统计某个站点的请求以及响应时间。不过最好建议有专门的日志系统去处理,可以获取更完善的,更定制化的信息。另外,traefik 无法查看请求的 body。# 筛选特定站点的请求cat traefik-access.json | jq ‘select(.[“RequestHost”] == “shici.xiange.tech”) | {RequestPath, RequestHost, DownstreamStatus, “request_User-Agent”, OriginDuration}’# 筛选大于 300ms 的接口cat traefik-access.json | jq ‘select(.[“RequestHost”] == “shici.xiange.tech” and .OriginDuration > 300000000) | {RequestPath, RequestHost, DownstreamStatus,“request_User-Agent”, OriginDuration, DownstreamContentSize}‘prometheus + grafanajq 虽然可以分析日志,但是适合做日志的统计以及更细化的分析。Prometheus 作为时序数据库,可以用来监控 traefik 的日志,支持更加灵活的查询,报警以及可视化。traefik 默认设置 prometheus 作为日志收集工具。另外可以使用 grafana 做为 prometheus 的可视化工具。某个服务的平均响应时间PromQL 为sum(traefik_backend_request_duration_seconds_sum{backend="$backend”}) / sum(traefik_backend_requests_total{backend="$backend"}) * 1000某个服务响应时长大于 300ms 的请求的个数TODO统计请求数大于 10000 的服务TODOentryPointhttphttp 配置在 entryPoints 中,暴露出80端口。开启 gzip 压缩,使用 compress = true 来配置。[entryPoints] [entryPoints.http] address = “:80” compress = true # 如果配置了此项,会使用 307 跳转到 https [entryPoints.http.redirect] entryPoint = “https"考虑到隐私以及安全,不对外公开的服务可以配置 Basic Auth,Digest Auth 或者 WhiteList,或者直接搭建 VPN,在内网内进行访问。如在我服务器上 xiange.tech 对外公开,xiange.me 只能通过VPN访问。更多文档查看 Traefik entrypoints。https使用 Let’s Encrypt 安装证书后,在 entryPoints.https.tls.certificats 中指定证书位置。[entryPoints] [entryPoints.https] address = “:443” compress = true [[entryPoints.https.tls.certificates]] certFile = “/etc/letsencrypt/live/xiange.tech/fullchain.pem” keyFile = “/etc/letsencrypt/live/xiange.tech/privkey.pem"另外,traefik 默认开启 http2。other另外,如果需要暴露其它的端口出去,如 consul 的 8500,类似于 nginx 的 listen 指令。可以设置[entryPoints] [entryPoints.consul] address = “:8500"Dockertraefik 会监听 docker.sock,根据容器的 label 进行配置。容器的端口号需要暴露出来,但是不需要映射到 Host。因为 traefik 可以通过 docker.sock 找到 container 的 IP 地址以及端口号,无需使用 docker-proxy 转发到 Host。version: ‘3’services: frontend: image: your-frontend-server-image labels: - “traefik.frontend.rule=Host:frontend.xiange.tech” api: image: your-api-server-image expose: 80 labels: # 同域配置, /api 走server - “traefik.frontend.rule=Host:frontend.xiange.tech;PathPrefix:/api"如何给一个服务配置多个域名labels: - “traefik.prod.frontend.rule=Host:whoami.xiange.tech” - “traefik.another.frontend.rule=Host:who.xiange.tech” - “traefik.dev.frontend.rule=Host:whoami.xiange.me"如何把前端和后端配置在统一域名services: frontend: image: your-frontend-server-image labels: - “traefik.frontend.rule=Host:frontend.xiange.tech” api: image: your-api-server-image expose: 80 labels: - “traefik.frontend.rule=Host:frontend.xiange.tech;PathPrefix:/api"部署时,如果项目代码有更新,如何当新服务 start 后,再去 drop 掉旧服务TODO负载均衡如果使用docker,对一个容器进行扩展后,traefik 会自动做负载均衡,而 nginx 需要手动干预。version: ‘3’services: whoami: image: emilevauge/whoami labels: - “traefik.frontend.rule=Host:whoami.xiange.tech"手动扩展为3个实例,可以自动实现负载均衡。实现效果可以直接访问 whoami.xiange.tech,每次通过 WRR 策略分配到不同的容器处理,可以通过 Hostname 和 IP 字段看出。docker-compose up whoami=3手动配置当然,以上反向代理配置都是基于 docker,那如何像 nginx 一样配置呢。如把 consul.xiange.me 转发到 8500 这个端口。可以利用 traefik 的 file provider。[file] [backends] # consul 是服务的名字,也可以叫张三,也可以叫李四 [backends.consul] [backends.consul.servers] [backends.consul.servers.website] url = “http://0.0.0.0:8500” weight = 1 [frontends] [frontends.consul] entryPoints = [“http”] backend = “consul” [frontends.consul.routes] # website 是路由的名字,也可以叫阿猫,也可以叫阿狗 [frontends.consul.routes.website] rule = “Host:consul.xiange.me” # 可以配置多个域名 [frontends.consul.routes.website2] rule = “Host:config.xiange.me” ...

February 3, 2019 · 2 min · jiezi

后端相关技能(六):压力测试

预期学习目标压力测试的概念常见的压力测试工具实例相关文章后端相关技能(一):数据库后端相关技能(二):Vue框架后端相关技能(三):正则表达式后端相关技能(四):计算机网络后端相关技能(五):Node.js后端相关技能(六):压力测试

February 2, 2019 · 1 min · jiezi

利用Nginx反向代理解决跨域问题

问题在之前的分享的跨域资源共享的文章中,有提到要注意跨域时,如果要发送Cookie,Access-Control-Allow-Origin就不能设为*,必须指定明确的、与请求网页一致的域名。在此次项目开发中与他人协作中就遇到此类问题。解决思路一般来说,与后台利用CORS跨域资源共享将Access-Control-Allow-Origin设置为访问的域名即可,这个需要后台的配合,且有些浏览器是不支持的。基于与合作方后台的配合,利用nginx方向代理来满足浏览器的同源策略来实现跨域实现方法反向代理概念反向代理(Reverse Proxy)方式是指以代理服务器来接受Internet上的连接请求,然后将请求转发给内部网络上的服务器;并将从服务器上得到的结果返回给Internet上请求连接的客户端,此时代理服务器对外就表现为一个服务器。反向代理服务器对于客户端而言它就像是原始服务器,并且客户端不需要进行任何特别的设置。客户端向反向代理的命名空间(name-space)中的内容发送普通请求,接着反向代理将判断向何处(原始服务器)转交请求,并将获得的内容返回给客户端,就像这些内容原本就是它自己的一样。利用nginx反向代理实现跨域的步骤去nginx官网下载包搭建nginx环境修改nginx的配置文件,找到ngixn.conf文件,修改相关配置http { include mime.types; default_type application/octet-stream; sendfile on; server { listen 8000; #监听8000端口,可以改成其他端口 server_name localhost; # 当前服务的域名 location /wili/api/ { proxy_pass http://chick.platform.deva.wili.us/api/; #添加访问路径录为/will/api的代理配置 proxy_http_version 1.1; } location / { proxy_pass http://localhost:8001; proxy_http_version 1.1; } error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } }}配置的解释:由配置信息可知,我们让nginx监听localhost的8000端口,网站A与网站B的访问都是经过localhost的8000端口进行访问。我们特殊配置了一个"/will/api"的访问,使以"will/api”开头的地址都转到"http://chick.platform.deva.wili.us/api/“进行处理。访问地址修改既然我们已经配置了nginx,那么所有的访问都要走nginx,而不是走网站原本的地址(A网站localhost:8001,B网站http://chick.platform.deva.wi…)。所以要修改A网站中的请求接口换成http://localhost:8000/wili/api/。接下来启动nginx,访问配置的8000即可需要注意的一点是nginx启动可能会冲突端口造成启动不成功,可在任务管理器查看是否启动成功。总结浏览器跨域的解决方式有很多种:jsonp 需要目标服务器配合一个callback函数CORS需要服务器设置header:Access-Control-Allow-Originnginx反向代理 这个方法一般很少有人提及,但是他可以不用目标服务器配合,不过需要你搭建一个中转nginx服务器,用于转发请求。(使用反向代理可能访问网页相对于之前响应会比较慢)

January 31, 2019 · 1 min · jiezi

nginx 代理服务器配置双向证书验证

生成证书链用脚本生成一个根证书, 一个中间证书(intermediate), 三个客户端证书.脚本来源于(有修改)https://stackoverflow.com/que…中间证书的域名为 localhost.#!/bin/bash -xset -efor C in echo root-ca intermediate; do mkdir $C cd $C mkdir certs crl newcerts private cd .. echo 1000 > $C/serial touch $C/index.txt $C/index.txt.attr echo ‘[ ca ]default_ca = CA_default[ CA_default ]dir = ‘$C’ # Where everything is keptcerts = $dir/certs # Where the issued certs are keptcrl_dir = $dir/crl # Where the issued crl are keptdatabase = $dir/index.txt # database index file.new_certs_dir = $dir/newcerts # default place for new certs.certificate = $dir/cacert.pem # The CA certificateserial = $dir/serial # The current serial numbercrl = $dir/crl.pem # The current CRLprivate_key = $dir/private/ca.key.pem # The private keyRANDFILE = $dir/.rnd # private random number filenameopt = default_cacertopt = default_capolicy = policy_matchdefault_days = 365default_md = sha256[ policy_match ]countryName = optionalstateOrProvinceName = optionalorganizationName = optionalorganizationalUnitName = optionalcommonName = suppliedemailAddress = optional[req]req_extensions = v3_reqdistinguished_name = req_distinguished_name[req_distinguished_name][v3_req]basicConstraints = CA:TRUE’ > $C/openssl.confdoneopenssl genrsa -out root-ca/private/ca.key 2048openssl req -config root-ca/openssl.conf -new -x509 -days 3650 -key root-ca/private/ca.key -sha256 -extensions v3_req -out root-ca/certs/ca.crt -subj ‘/CN=Root-ca’openssl genrsa -out intermediate/private/intermediate.key 2048openssl req -config intermediate/openssl.conf -sha256 -new -key intermediate/private/intermediate.key -out intermediate/certs/intermediate.csr -subj ‘/CN=localhost.‘openssl ca -batch -config root-ca/openssl.conf -keyfile root-ca/private/ca.key -cert root-ca/certs/ca.crt -extensions v3_req -notext -md sha256 -in intermediate/certs/intermediate.csr -out intermediate/certs/intermediate.crtmkdir outfor I in seq 1 3 ; do openssl req -new -keyout out/$I.key -out out/$I.request -days 365 -nodes -subj “/CN=$I.example.com” -newkey rsa:2048 openssl ca -batch -config root-ca/openssl.conf -keyfile intermediate/private/intermediate.key -cert intermediate/certs/intermediate.crt -out out/$I.crt -infiles out/$I.requestdone服务器nginx 配置worker_processes 1;events { worker_connections 1024;}stream{ upstream backend{ server 127.0.0.1:8080; } server { listen 8888 ssl; proxy_pass backend; ssl_certificate intermediate.crt; ssl_certificate_key intermediate.key; ssl_verify_depth 2; ssl_client_certificate root.crt; ssl_verify_client optional_no_ca; }}客户端curl \ -I \ -vv \ -x https://localhost:8888/ \ –proxy-cert client1.crt \ –proxy-key client1.key \ –proxy-cacert ca.crt \ https://www.baidu.com/ ...

January 31, 2019 · 2 min · jiezi

Centos7 安装 Odoo11

Centos7 安装 Odoo111 安装python3.6Centos7 基于稳定性考虑安装的是python2.7,而且默认的官方 yum 源中不提供 Python 3 的安装包,所以我们要先换一个提供python3的yum源– IUS 。 1、IUS软件源依赖与epel软件源包,首先要安装epel软件源包sudo yum install epel-release2、安装IUS软件源sudo yum install https://centos7.iuscommunity.org/ius-release.rpm3、安装python3.6sudo yum install python36usudo yum -y install python36u-develsudo yum -y install python36u-pip2 安装配置PostgreSQL数据库2.1 安装1、安装sudo yum install -y postgresql-server2、初始化service postgresql initdb3、启动服务systemctl start postgresql4、设置开机运行服务systemctl enable postgresql2.2 配置1、创建数据库和角色# 切换到 postgres 用户sudo su - postgres# 登录PostgreSQL控制台psql# 系统提示符会变为"postgres=#",表示这时已经进入了数据库控制台# 创建数据库用户dbuserCREATE USER dbuser WITH PASSWORD ‘password’ ENCODING=‘UTF8’;# 创建用户数据库CREATE DATABASE exampledb OWNER dbuser;# 将exampledb数据库的所有权限都赋予dbuserGRANT ALL PRIVILEGES ON DATABASE exampledb to dbuser;# 使用\q命令退出控制台(也可以直接按ctrl+D)\q如果在创建数据库时报如下错误:ERROR: new encoding (UTF8) is incompatible with the encoding of the template database (SQL_ASCII)则通过如下方式解决update pg_database set datallowconn = TRUE where datname = ’template0’; \c template0update pg_database set datistemplate = FALSE where datname = ’template1’; drop database template1;create database template1 with encoding = ‘UTF8’ LC_CTYPE = ’en_US.UTF-8’ LC_COLLATE = ’en_US.UTF-8’ template = template0;update pg_database set datallowconn = TRUE where datname = ’template1’;\c template1update pg_database set datallowconn = FALSE where datname = ’template0’;——————— 作者:东方-phantom 来源:CSDN 原文:https://blog.csdn.net/hkyw000/article/details/52817422 版权声明:本文为博主原创文章,转载请附上博文链接!2、配置这一步要修改两个配置文件:pg_hba.conf 和 postgresql.conf 。可以通过以下命令找到文件位置:sudo find / -name ‘filename’首先修改 pg_hba.conf :添加下面这行(这行是用于可远程连接的,如果想限制数据库只能本地访问的话,跳过)host all all 0.0.0.0/0 md5找到并修改下面这两行local all all peer md5host all all 127.0.0.1/32 ident md5修改 postgresql.conf (用于可远程连接,如不需要可调过):添加下面这行listen_addresses = ‘*‘修改完成之后,重启服务:systemctl restart postgresql至此,PostgreSQL 安装配置完成!3 安装 node.js 和 less插件Odoo 前端依赖 node.js 和 less,用以下命令安装:sudo yum install -y nodejssudo npm install -g less less-plugin-clean-css4 安装依赖yum install wkhtmltopdfyum install python-devel openldap-develyum install libxslt-devel libxml++-devel libxml2-develyum install gcc5 安装Odoo11这里我们用 pipenv 安装,首先安装 pipenvpip3.6 install pipenv拉取odoo11 代码后,在项目根目录创建虚拟环境并安装依赖pipenv –python python3.6 install -r requirments.txt安装时会有一个 win32 的模块安装失败,不用管,这个是windows系统开发时需要依赖的包。安装完成之后,创建一个 odoo 配置文件: odoo.conf 。 内容如下:[options];模块路径addons_path = odoo/addons,odoo/myaddons;超级管理员密码admin_passwd = admindb_host = localhostdb_port = 5432db_maxconn = 64;数据库名称db_name = ***;数据库用户db_user = ***;数据库密码db_password = ***然后运行如下命令启动 odoopython odoo-bin -c odoo.conf访问 127.0.0.1:8069 ,如果进入到odoo登录页面就说明安装成功了! ...

January 31, 2019 · 2 min · jiezi

2分钟获得HTTPS证书

2分钟获得HTTPS证书我们将利用Certbot来获取Let’s Encrypt的免费HTTPS证书。这里是使用的是CentOS 6 + ngibx,更多的系统的安装方法可以到Certbot官网查询,这里就不一一赘述。安装Certbot因为CentOS 6没有Certbot的打包版本,所以我们需要用certbot-auto脚本来获取。wget https://dl.eff.org/certbot-autochmod a + x certbot-auto运行Certbot现在我们就可以直接运行Certbot来获取我们需要的HTTPS证书了。这里要注意一点Certbot默认的nginx.conf的路径是/etc/nginx/nginx.conf。如果你的conf文件是在该路径下则直接运行Certbot即可。$ sudo ./certbot-auto –nginx如果你的conf文件在其他路径下,则需要使用nginx-server-root参数指定conf文件的路径。$ sudo ./certbot-auto –nginx –nginx-server-root=/usr/local/nginx/conf接下来又到了我们最喜欢的无脑yes下一步环节了。(你要认真看问题也是可以滴)选择了激活的站点和重定向之后,它就会帮我修改nginx.conf文件并开心的恭喜我成功啦。HTTPS证书相关的文件存放在了/etc/letsencrypt/里。这时我们打开我们的站点,发现它已经变成了金色传说的https了。

January 29, 2019 · 1 min · jiezi

Nginx反向代理理解误区之proxy_cookie_domain

基本内容Nginx做反向代理的时候,我们一般习惯添加proxy_cookie_domain配置,来做cookie的域名转换,比如…location /api { proxy_pass https://b.test.com; proxy_cookie_domain b.test.com a.test.com;} …在之前的博客中我也是这么写的,但是最近在项目中发现,不配置这个属性,依然运转正常,背后冷风阵阵,我发现自己一直以来可能又理解错了这个选项,然后还在这给别人讲。。。我们首先来看下proxy_cookie_domain的官方定义,Syntax: proxy_cookie_domain off;proxy_cookie_domain domain replacement;Default: proxy_cookie_domain off;Context: http, server, locationThis directive appeared in version 1.1.15.Sets a text that should be changed in the domain attribute of the “Set-Cookie” header fields of a proxied server response. Suppose a proxied server returned the “Set-Cookie” header field with the attribute “domain=localhost”. The directive proxy_cookie_domain localhost example.org will rewrite this attribute to “domain=example.org”.翻译过来就是proxy_cookie_domain参数的作用是转换response的set-cookie header中的domain选项,由后端设置的域名domain转换成你的域名replacement,来保证cookie的顺利传递并写入到当前页面中,注意proxy_cookie_domain负责的只是处理response set-cookie头中的domain属性,仅此而已。但是我们知道response在写set-cookie的时候,domain是一个可选项,并不是必填项,所以经常能看到如下这种情况这个时候由于set-cookie本身就没有domain内容,proxy_cookie_domain也就不没有必要了,这也是为什么在部分项目中不配置proxy_cookie_domain依然正常的原因。但是对于一些设置了domain的项目,比如这种情况下当你用nginx做反向代理的时候,就必须要转换一下了。误区回溯说到这里,我们再看看之前的错误理解:“proxy_cookie_domain的作用是实现前后端cookie域名转换,保证顺利传递”乍一看好像也没错,但是现在想想,理解还是不够啊,因为proxy_cookie_domain的作用是单向的,并不是双向转换的。我们先看下cookie的传递过程,盗一张图先(懒得画了。。。)浏览器在发送请求的时候,会在request header中带上cookie项(有内容的话),此时的cookie是一个字符串,一个key=value并用分号分割的字符串,其中并不包含任何域名信息。这是因为浏览器在设置cookie选项的时候,所选取的内容都是缓存中接口域名下的。然后request的只要请求发送出去之后,cookie中有关domain信息其实是不存在的,它只是一个普通的字符串,随便proxy_pass到任何位置,都会正常携带下去。因此在前端到后端的request的过程中,proxy_cookie_domain是没用的而server端在做响应的时候,通过set-cookie的domain属性,可以控制cookie的生效域名目标,做到诸如二级域名cookie分离等等,如果前端接收到的set-cookie的domain和当前域名不一致,或者一级域名不一致(二级域名可以共享一级域名下的cookie),这个cookie在后续的通信中就是无效的,所以这里才需要去做domain的转换,也就是说response中set-cookie的domain转换才是有意义的,这也正是proxy_cookie_domain的作用所在。当reseponse的set-cookie中domain不去设置时,cookie顺利传入浏览器中,浏览器会自动设置这个cookie的生效域名为当前域名。和这个类似的还有proxy_cookie_path属性,同样的该属性仅作用在修改response set-cookie的path属性,而一般情况下,用的也比较少。唠叨两句很多问题,有时候都是太过理所当然的以为它是怎么样的,并且生效了、达到目的了,我们就认为它是这样的了,但往往大脸就会在后面不期而至。多学习,多去关注一些底层的原理,才会发现自己理解的错误,望诸君共勉~如果错误,欢迎指出~ ...

January 29, 2019 · 1 min · jiezi

PHP-FPM 与 Nginx 的通信机制总结

PHP-FPM 介绍CGI 协议与 FastCGI 协议每种动态语言( PHP,Python 等)的代码文件需要通过对应的解析器才能被服务器识别,而 CGI 协议就是用来使解释器与服务器可以互相通信。PHP 文件在服务器上的解析需要用到 PHP 解释器,再加上对应的 CGI 协议,从而使服务器可以解析到 PHP 文件。由于 CGI 的机制是每处理一个请求需要 fork 一个 CGI 进程,请求结束再kill掉这个进程,在实际应用上比较浪费资源,于是就出现了CGI 的改良版本 FastCGI,FastCGI 在请求处理完后,不会 kill 掉进程,而是继续处理多个请求,这样就大大提高了效率。PHP-FPM 是什么PHP-FPM 即 PHP-FastCGI Process Manager, 它是 FastCGI 的实现,并提供了进程管理的功能。进程包含 master 进程和 worker 进程两种;master 进程只有一个,负责监听端口,接收来自服务器的请求,而 worker 进程则一般有多个(具体数量根据实际需要进行配置),每个进程内部都会嵌入一个 PHP 解释器,是代码真正执行的地方。Nginx 与 php-fpm 通信机制当我们访问一个网站(如 www.test.com)的时候,处理流程是这样的: www.test.com | | Nginx | |路由到www.test.com/index.php | |加载nginx的fast-cgi模块 | |fast-cgi监听127.0.0.1:9000地址 | |www.test.com/index.php请求到达127.0.0.1:9000 | | 等待处理…Nginx 与 php-fpm 结合在 Linux 上,Nginx 与 php-fpm 的通信有 tcp 和 unix socket 两种方式。tcp 的优点是可以跨服务器,当 Nginx 和 PHP-fpm 不在同一台机器上时,只能使用这种方式。Unix socket 又叫 IPC(inter-process communication 进程间通信) socket,用于实现同一主机上的进程间通信,这种方式需要在 nginx配置文件中填写 php-fpm 的 socket 文件位置。两种方式的数据传输过程如下图所示:二者的不同:由于 Unix socket 不需要经过网络协议栈,不需要打包拆包、计算校验和、维护序号和应答等,只是将应用层数据从一个进程拷贝到另一个进程。所以其效率比 tcp 的方式要高,可减少不必要的 tcp 开销。不过,Unix socket 高并发时不稳定,连接数爆发时,会产生大量的长时缓存,在没有面向连接协议的支撑下,大数据包可能会直接出错不返回异常。而 tcp 这样的面向连接的协议,可以更好的保证通信的正确性和完整性。Nginx 与 php-fpm 结合只需要在各自的配置文件中做设置即可:1) Nginx 中的配置以 tcp 通信为例server { listen 80; #监听80端口,接收http请求 server_name www.test.com; #就是网站地址 root /usr/local/etc/nginx/www/huxintong_admin; # 准备存放代码工程的路径 #路由到网站根目录www.test.com时候的处理 location / { index index.php; #跳转到www.test.com/index.php autoindex on; } #当请求网站下php文件的时候,反向代理到php-fpm location ~ .php$ { include /usr/local/etc/nginx/fastcgi.conf; #加载nginx的fastcgi模块 fastcgi_intercept_errors on; fastcgi_pass 127.0.0.1:9000; # tcp 方式,PHP-fpm 监听的IP地址和端口 # fasrcgi_pass /usr/run/php-fpm.sock # unix socket 连接方式 }}2) php-fpm 的配置listen = 127.0.0.1:9000# 或者下面这样listen = /var/run/php-fpm.sock注意,在使用 Unix socket 方式连接时,由于 socket 文件本质上是一个文件,存在权限控制的问题,所以需要注意 Nginx 进程的权限与 php-fpm 的权限问题,不然会提示无权限访问。(在各自的配置文件里设置用户)通过以上配置即可完成 php-fpm 与 Nginx 的通信。 ...

January 28, 2019 · 1 min · jiezi

Nginx入门及如何反向代理解决生产环境跨域问题

1.Nginx入门与基本操作篇注:由于服务器是windows系统,所以本文主要讲解Nginx在windows下的操作。首先下载Nginx解压缩,我们所有的配置基本都在万能的 nginx/conf/nginx.conf 中完成,其它文件可以不用理关于nginx.conf…#需要我们按需要修改的一般只有中间server里的代码 server { # 设置监听端口 listen 9000; server_name localhost; # 设置静态资源路径,如下设置打输入地址时会打开H盘app/dist下的index页面 location / { root H:/app/dist; index index.html; } # 报错页面,同上根据需要设置,也可不理 error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } }…这样一个简单服务的配置就完成了,启动服务访问 http://localhost:9000 即可打开对应网页,如果需要启动多个服务,只需多添加server配置后重启即可。几个常用的操作指令(以下指令请在nginx文件目录下使用)# 启动nginx(我的情况是运行起来后cmd就输入不了其它指令了,需要在打开一个cmd来操作) nginx.exe# 修改nginx.conf文件后重启nginx nginx.exe -s reload# 暂停nginx服务 nginx.exe -s stop# 有时暂停服务失效,需要强制终止nginx进程 注:/f 强制执行 taskkill /im nginx.exe /f2.反向代理解决跨域问题通常开发环境可以通过设置proxy解决跨域问题,而生产环境下要么把前端项目放在后端项目里,要么设置cor解决跨域问题,前者不利于前后端分离,后者需要后端配置,而现在使用nginx做启动服务设置反向代理可以很好解决跨域问题。还是回到万能的nginx.conf文件,添加location匹配规则实现代理转发 server { listen 9000; server_name localhost; location / { root H:/app/dist; index index.html; } #设置代理转发 location /api/ { proxy_pass http://localhost:9600/; } error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } }通过上面的设置,在重启服务,可以让页面中所有包含 /api/ 字段的请求都转为由服务器去向http://localhost:9600/地址发送请求,从而巧妙的解决了浏览器的跨域问题。怎么样,是不是很简单,快动手配置是试试吧(^_^)~ ...

January 25, 2019 · 1 min · jiezi

从一起丢包故障来谈谈 nginx 中的 tcp keep-alive

一、故障基本架构如图所示,客户端发起 http 请求给 nginx,nginx 转发请求给网关,网关再转发请求到后端微服务。故障现象是,每隔十几分钟或者几个小时不等,客户端就会得到一个或者连续多个请求超时错误。查看 nginx 日志,对应请求返回 499;查看网关日志,没有收到对应的请求。从日志分析,问题应该处在 nginx 或者 spring-cloud-gateway 上。nginx 版本:1.14.2,spring-cloud 版本:Greenwich.RC2。nginx 主要配置如下:[root@wh-hlwzxtest1 conf]# cat nginx.confworker_processes 8;events { use epoll; worker_connections 10240;}http { include mime.types; default_type application/octet-stream; sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; #gzip on; upstream dbg2 { server 10.201.0.27:8888; keepalive 100; } server { listen 80; server_name localhost; charset utf-8; location /dbg2/ { proxy_pass http://dbg2/; proxy_http_version 1.1; proxy_set_header Connection “”; } }}为了提高性能,nginx 发送给网关的请求为 http 1.1,可以复用 tcp 连接。二、排查1、查看 tcp 连接[root@10.197.0.38 logs]# ss -n | grep 10.201.0.27:8888tcp ESTAB 0 0 10.197.0.38:36674 10.201.0.27:8888tcp ESTAB 0 0 10.197.0.38:40106 10.201.0.27:8888[root@10.201.0.27 opt]# ss -n | grep 10.197.0.38tcp ESTAB 0 0 ::ffff:10.201.0.27:8888 ::ffff:10.197.0.38:40106tcp ESTAB 0 0 ::ffff:10.201.0.27:8888 ::ffff:10.197.0.38:39266可以看到 nginx 和网关之间建立的 socket 连接为 (10.201.0.27:8888,10.197.0.38:40106),另外的 2 条记录就很可疑了。猜测原因是:一端异常关闭了 tcp 连接却没有通知对端,或者通知了对端但对端没有收到。2、抓包分析先看下 nginx 的抓包数据:序号 8403:转发 http 请求给网关;序号 8404:在 RTT 时间内没有收到 ack 包,重发报文;序号 8505:RTT 约等于 0.2s,tcp 重传;序号 8506:0.4s 没收到 ack 包,tcp 重传;序号 8507:0.8s 没收到 ack 包,tcp 重传;序号 8509:1.6s 没收到 ack 包,tcp 重传;…序号8439:28.1s(128RTT)没收到 ack 包,tcp 重传。序号 8408:请求设置了超时时间为 3s,因此发送 FIN 包。再看下网关的抓包数据:序号 1372:17:24:31 收到了 nginx 发过来的 ack 确认包,对应 nginx 抓包图中的序号 1348(nginx 那台服务器时间快了差不多 1 分 30 秒);序号 4221:2 小时后,发送 tcp keep-alive 心跳报文,(从 nginx 抓包图中也可以看出这 2 小时之内该 tcp 连接空闲);序号 4253:75s 后再次发送 tcp keep-alive 心跳;序号 4275:75s 后再次发送心跳;连续 9 次;序号 4489:发送 RST 包,通过对端重置连接。2 小时,75s, 9 次,系统默认设置。[root@eureka2 opt]# cat /proc/sys/net/ipv4/tcp_keepalive_time7200[root@eureka2 opt]# cat /proc/sys/net/ipv4/tcp_keepalive_intvl75[root@eureka2 opt]# cat /proc/sys/net/ipv4/tcp_keepalive_probes9具体这几个参数的作用,参考文章:为什么基于TCP的应用需要心跳包3、分析通过以上抓包分析,基本确认了问题出在 nginx 上。19:25 时,网关发送 tcp keep-alive 心跳包给 nginx 那台服务器,此时那台服务器上保留着该 tcp 连接,却没有回应;22:20 时,nginx 发送 http 请求给网关,而网关已经关闭该 tcp 连接,因此没有应答。三、解决1、proxy_send_timeoutnginx 中与 upstream 相关的超时配置主要有如下参数,参考:Nginx的超时timeout配置详解proxy_connect_timeout:nginx 与 upstream server 的连接超时时间;proxy_read_timeout:nginx 接收 upstream server 数据超时, 默认 60s, 如果连续的 60s 内没有收到 1 个字节, 连接关闭;proxy_send_timeout:nginx 发送数据至 upstream server 超时, 默认 60s, 如果连续的 60s 内没有发送 1 个字节, 连接关闭。这几个参数,都是针对 http 协议层面的。比如 proxy_send_timeout = 60s,并不是指如果 60s 没有发送 http 请求,就关闭连接;而是指发送 http 请求后,在两次 write 操作期间,如果超过 60s,就关闭连接。所以这几个参数,显然不是我们需要的。2、upstream 模块的 keepalive_timeout 参数查看官网文档,Module ngx_http_upstream_module,Syntax: keepalive_timeout timeout;Default: keepalive_timeout 60s;Context: upstreamThis directive appeared in version 1.15.3.Sets a timeout during which an idle keepalive connection to an upstream server will stay open.设置 tcp 连接空闲时间超过 60s 后关闭,这正是我们需要的。为了使用该参数,升级 nginx 版本到 1.15.8,配置文件如下:http { upstream dbg2 { server 10.201.0.27:8888; keepalive 100; keepalive_requests 30000; keepalive_timeout 300s; } …}设置 tcp 连接上跑了 30000 个 http 请求或者空闲 300s,那么就关闭连接。之后继续测试,没有发现丢包。序号 938:空闲 5 分钟后,nginx 主动发起 FIN 报文,关闭连接。 ...

January 24, 2019 · 2 min · jiezi

php伪造Referer请求反盗链资源

有些产品为了防止自己的产品被盗链访问,会采用反盗链措施,如封闭型生态的音乐网站和视频网站,他们已经为了版权付费,自然不希望你免费使用他们的资源。但因为很多人专门研究盗链,因此我们也需要了解下盗链、反盗链和逃避反盗链的原理。盗链引用百度百科对盗链的定义:盗链是指服务提供商自己不提供服务的内容,通过技术手段绕过其它有利益的最终用户界面(如广告),直接在自己的网站上向最终用户提供其它服务提供商的服务内容,骗取最终用户的浏览和点击率。受益者不提供资源或提供很少的资源,而真正的服务提供商却得不到任何的收益。常规盗链我们知道,网站提供服务是向服务端请求一个html文件,这个文件中包含有css/js文件,也包含img/video标签,这些静态资源会在html文件加载时,依次的发起请求并填充在指定位置上,从而完成整个页面的加载。因此只要拿到这个图片的URL并嵌入我们自己的html文件中,就能在我们的网站上访问,由于资源是不同的HTTP请求独立访问的,因此我们也能过滤源站的html文件。这就是最简单的盗链。危害:在用户访问时,并没有在访问被盗链网站,但是依然会占用该网站的带宽资源,而带宽是要给运营商付费的。同时,该网站的广告、周边、宣传等资源并不会被用户访问到。分布式盗链分布式盗链比较复杂,需要在服务端部署专门的程序,并不针对单个网站或单个url,而是对全网的所有有用的资源进行盗取,并存储在自己的数据库中,并在用户实际访问时,完全转换为自己的流量。危害:自己通过劳动、金钱、版权付费得到的资源,被盗链网站免费使用,如网店摄影图、期刊、电视剧等。并因此导致自己的会员、服务无法实现盈利。反盗链分类我们了解了盗链对源站的危害后,自然要通过一些手段来阻止这种行为维护自己的利益。加水印这是最简单的方法,通过后端程序批量对图片等资源加上水印,这样在盗链的同时,也在为自己的网站做宣传,有时甚至会主动寻求这种盗链。资源重命名因为盗链是通过指定的url,这个url中一定包含该资源的路径和名称,因此通过不定期的更改文件或目录的名称,能够快速避免盗链,但也会导致正在下载的资源被中断。限制引用页在http请求的头部信息中,有一个字段:referer,它代表这个请求是从哪个页面发起的,如果是单独在页面中打开或者服务端请求的,则这个字段为空。因此我们可以通过referer这个字段的值做限制,如果是自己认可的页面,则返回资源,否则,禁止该请求。但是由于每次都要打开一个白名单的文件做url匹配,因此会降低性能。加密认证在客户端通过将用户认证的信息和资源的名称进行组合后加密,将加密的字符串作为url的参数发起请求,在服务端进行解密并认证通过后,才会返回请求的资源。这个方式主要用于防范分布式盗链。反盗链程序上面的3种反盗链方式,我们常用的是第三种,通过referer属性来完成反盗链,今天也主要分享这一种方法的反盗链与防反盗链。后端程序限制这种限制需要消耗服务端计算资源,因此不如Nginx限制常用。$from = parse_url($_SERVER[‘HTTP_REFERER’]);if ($from[‘host’]!=‘xxx.com’ && $from[‘host’]!=‘www.xxx.com’) { die(‘你丫在盗链’);}Nginx限制通过修改nginx配置文件可以做到,修改完成后记得重启nginx:// 这里指定需要防盗链的资源,如gif/jpg等location ~* .(gif|jpg|png|jpeg)$ { // 设置资源的过期时间 expires 30d; // 设置合法的引用页,也就是防盗链的白名单; // none blocked保证用户在新页面打开时依然能够打开,如果不希望用户能够保存删掉这两项 valid_referers none blocked *.hugao8.com *.baidu.com *.google.com; // 对于非法的引用页,可以重写图片,也可以直接返回403或404页面 if ($invalid_referer) { rewrite ^/http://www.it300.com/static/images/404.jpg; #return 404; }}Referer-PolicyReferer 首部包含了当前请求页面的来源页面的地址,即表示当前页面是通过此来源页面里的链接进入的。服务端一般使用 Referer 首部识别访问来源,可能会以此进行统计分析、日志记录以及缓存优化等。Referer属性出现在请求头中,也在请求头中被设置,但是在浏览器的安全策略里,该值无法被js所指定:$.ajax({ url: ‘http://www.baidu.com’, beforeSend(xhr) { // 在发送ajax请求前设置header头部 xhr.setRequestHeader(“Referer”, “http://translate.google.com/"); xhr.setRequestHeader(“User-Agent”, “stagefright/1.2 (Linux;Android 5.0)”); }, success(data) { console.log(data); }, error(err) { console.log(err); }});然而浏览器会报错:那么Referer是怎么被自动设置的呢?这个得看Referer-Policy属性是怎么定义的:no-referrer: 整个 Referer 首部会被移除。访问来源信息不随着请求一起发送。no-referrer-when-downgrade(默认值): 在没有指定任何策略的情况下用户代理的默认行为。在同等安全级别的情况下,引用页面的地址会被发送(HTTPS->HTTPS),但是在降级的情况下不会被发送 (HTTPS->HTTP)。origin: 在任何情况下,仅发送文件的源作为引用地址。例如 https://example.com/page.html 会将 https://example.com/ 作为引用地址。origin-when-cross-origin: 对于同源的请求,会发送完整的URL作为引用地址,但是对于非同源请求仅发送文件的源。same-origin: 对于同源的请求会发送引用地址,但是对于非同源请求则不发送引用地址信息。strict-origin: 在同等安全级别的情况下,发送文件的源作为引用地址(HTTPS->HTTPS),但是在降级的情况下不会发送 (HTTPS->HTTP)。strict-origin-when-cross-origin: 对于同源的请求,会发送完整的URL作为引用地址;在同等安全级别的情况下,发送文件的源作为引用地址(HTTPS->HTTPS);在降级的情况下不发送此首部 (HTTPS->HTTP)。unsafe-url: 无论是同源请求还是非同源请求,都发送完整的 URL(移除参数信息之后)作为引用地址。这个值可以通过三种方式来设置:<meta name=“referrer” content=“origin”><a href=“http://example.com” referrerpolicy=“origin”><a href=“http://example.com” rel=“noreferrer”>防反盗链前端JS不能在头部设置Referer字段,和跨域一样是因为浏览器的安全策略,那么同样的在服务端进行请求就不会有这些限制,我们在服务端请求时就可以自由的修改Referer字段。我们通过简单的PHP例子来完成这个功能:<?php$url = ‘http://t11.baidu.com/it/u=3008889497,862090385&fm=77';$refer = ‘https://www.baidu.com’;$ch = curl_init();//以url的形式 进行请求curl_setopt($ch, CURLOPT_URL, $url);//以文件流的形式 进行返回 不直接输出到浏览器curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);//浏览器发起请求 超时设置curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30);//伪造来源地址 curl_setopt ($ch, CURLOPT_REFERER, $refer);$file = curl_exec($ch);curl_close($ch);header(‘Content-Type: text/html’);// 对图片进行base64编码,然后返回给前端展示$file = base64_encode($file);echo “<img src=‘data:image/jpeg;base64,{$file}’ />”;?>我们第一次请求注释了伪造来源地址这一行,第二次请求不注释这一行,这样可以验证执行结果:总结盗链和反盗链是一个对立面,技术不断升级,最终的目标也是为了开放资源和保护知识产权。在互联网生态里,我们通过反盗链保护我们的利益,也使用防反盗链的这种方式来扩大我们的内容,无论站在哪一方,都需要做到知己知彼。参考文章百科-盗链:https://baike.baidu.com/item/…php防盗链:https://segmentfault.com/q/10…Referer伪造:https://zhuanlan.zhihu.com/p/…nginx防盗链:https://www.jianshu.com/p/979… ...

January 23, 2019 · 1 min · jiezi

服务器从安装到部署全过程(二)

OK! 上一篇文章中大概尝试安装mysql、nginx、nodeJs、pm2 的一些过程以及在配置中出现的问题还有一些解决方式,那么在本次过程尝试中,将进一步进行服务器的基础配置,以及前端应用 node、pm2 部署nginx经过上次 nginx 已经安装至服务器中了,在这里面主要介绍一下部署相关所需要的配置,例如:监听端口、服务转发、静态资源、ssl 证书(https) 的配置静态资源在我们没有后端服务的时候,只是想要写一些demo或者静态页面时,就可以使用nginx作为静态资源服务器,可以被外网访问到,以下就是 具体的配置了,可以在nginx.conf 中建立二级域名,来搭建。http { server { listen 80; # 监听的端口号 server_name static.scrscript.com # 转发的名称 也就是二级域名 不过二级域名虽然不需要重新备案但是 域名解析要有 static 关键字 location / { root /cloud/static } }}说明: 监听的 80 端口 然后 转发名称为 server_name 所对应的静态路径为 /cloud/static/ 其实就是 location root + localtioon name 这样就nginx就可以读取到 /cloud/static/ 目录下的文件了,server_name 设置二级域名时前缀后面最好跟的是自己的域名 自定义前缀+自己的域名,而域名解析最好要加上得以匹配其他二级域名来访问服务器静态的资源的转发 其实有两种写法:location / { root /cloud/static # 这种写法 主要是 location root + location name # alias /cloud/static # 这种写法就睡 就是直接location alias 的路径信息了}检验一下服务转发当我们有后端服务时, 我们那么我们可以通过nginx的转发配置,来让我们的网站可以被访问到http { # 使用upstream(上游)模块 # server 监听的端口 # weight 权重 # max_fails 最大允许失败数 # fail_timeout max_fails次失败后,暂停时间 upstream blog { #ip_hash; server localhost:9931 weight=2 max_fails=2 fail_timeout=3s; } server { listen 80 default; server_name _; location / { proxy_pass: http://blog; } }}说明:在请求的过程中 如果请求的地址 与 server_name 匹配上那么就会采用这个配置 如果都不匹配 那么就会先行查看 listen 监听的域名 后有default 配置项的,注意 default 只能写一个。转发说明:upstream 中的 server 监听的是服务器中启动的端口当请求匹配上 server server_name 时 就会 进行匹配 location在匹配成功 location 中的反向代理proxy_pass 为 http:// + upstream 时就会代理到 upstream 中的 server 监听的端口检验一下nginx 配置SSL(https)平常我们部署的时候,会发现浏览器的地址栏会有(http://) 然后会有不安全的字样, 哼~!作为强迫症可是不想有这种字样,那么我们就来配置https 安全吧首先要先去购买 ssl 证书当然还是要买免费的啦,个人需求,嘿嘿~当购买完成以后,需要一定时间来等待证书签发,如果签发完成了以后,就来下载证书信息因为我们转发是使用 nginx 来写的 所以应该使用 nginx 版本证书类型,下载完成后,需要把这些证书放在服务器中,当然证书的位置还是要放在 nginx.conf 所在的文件夹中然后我们就开始 修改 nginx.conf 文件吧http { # 使用upstream(上游)模块 # server 监听的端口 # weight 权重 # max_fails 最大允许失败数 # fail_timeout max_fails次失败后,暂停时间 upstream blog { #ip_hash; server localhost:9931 weight=2 max_fails=2 fail_timeout=3s; } server { listen 443 ssl; # 一定要开启 443 服务器端口 server_name xxx.com; # 需要ssl的域名 ssl on; # root /cloud/node/; # index index.html index.htm; ssl_certificate cert/a.pem; # ssl 证书在服务器中相对于 nginx.conf 的路径 ssl_certificate_key cert/a.key; # ssl 证书在服务器中相对于 nginx.conf 的路径 ssl_session_timeout 5m; ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_prefer_server_ciphers on; location / { proxy_pass http://blog; } } server { listen 80 default; server_name _; rewrite ^(.)$ https://$host$1 permanent; # 重定向到 https location / { proxy_pass: http://blog; } }}注意一定要 重启 nginx nginx -s reload哈哈哈哈 小锁 还有 https前端部署在这里 着重来说一下 vue 项目的部署过程,还有 html 文件怎么在浏览器中访问到html 静态资源访问其实在上面 nginx 中,我们配置了 nginx 静态资源服务器,其实静态资源的部署 非常简单 就是直接把 html 文件 放在静态资源所配置的目录就可以了, 这样我们平时写的项目 或者 demo 都可放在这个目录下,来实现浏览vue 单页应用部署blog 的 后端是使用的是 node-express 脚手架,在 express 脚手架中 express.static 定义的是 静态资源所在的位置,我们可以直接放在 项目的根目录下 然后启动项目 就可以直接访问到了。 静态资源访问路径: app.use(’’ ,express.static(path.join(__dirname, ‘dist’))); 请看我的项目结构 上传至 服务器 后 我们可以用 pm2 来启动 达到负载均衡, 守护进程的效果 pm2 start xxx检验一下vue nuxt 部署nuxt 是 vue 的 服务端渲染,有利于 seo 的服务端渲染应用框架,作为前台应用 当然是想要自己写的文章 能让更多人搜索到,nuxt 当然是我的不二之选,哈哈哈哈,OK 我们来说下 nuxt 如何来部署吧首先 我们应该先给 nuxt 应用执行打包命令 npm run build 打包完成后,我们可以把有用的文件事先剔除出来 下面请看我的文件目录 先上传至服务器然后 npm install or cnpm i然后进入到相应的 应用文件目录 执行 pm2 start npm –name “进程名称” – run start部署完毕 blog nuxt链接配置nginx静态服务器nginx server 匹配顺序nuxt 部署参考结语基础的配置 基本上已经走完一遍了,其实如果是 部署频率低的情况下 那么这样一遍遍部署 其实是费不了多大事,但是呢,在初期的时候总有不少东西要改,那这样一遍遍部署 不仅繁琐 而且还浪费我们 宝贵的时间那么,下次我将配置 docker and jenkins 自动化部署,省时又省力~,哈哈哈 ...

January 20, 2019 · 2 min · jiezi

记录manjaro linux下起angular + spring + nginx 项目环境

我用的linux是manjaro,内核是arch,有些东西和ubantu,deepin不太一样,所以在起环境时遇到了一些没见过的问题。安装nodejs与npm这俩我尝试下载安装官网上的包,但均以失败告终,原因不明,还好manjaro自带的软件库有最新版,可以很方便地进行安装。甚至还有检查最新版本依赖的工具,省下了手动更新的麻烦。接着通过一下命令完成全局安装 Angular CLI:npm install -g @angular/cli安装nginx这里我犯了一个错,在nginx官网上,我并没有注意自己的linux版本,就直接下载了一个linux包,结果配置半天各种失败,但官网上明确标示了各linux版本安装的方法。遗憾的是nginx官网上并没有提供manjaro/arch版的包,但其实可以在manjaro的软件库当中找到(不得不说这个软件库是真的强大,啥都有),或者通过以下命令进行安装:pacman -S nginx-mainline安装完后使用命令nging -t 会报如下错误:nginx: [warn] could not build optimal types_hash, you should increase either types_hash_max_size: 2048 or types_hash_bucket_size: 64; ignoring types_hash_bucket_size解决方法:在etc/nginx下找到nginx配置文件nginx.conf,在如下位置添加配置信息,顺便include项目需要的nginx配置文件types_hash_max_size 2048;server_names_hash_max_size 2068;types_hash_bucket_size 1024;接着nginx -t 测试配置文件是否正确。git配置设置git用户名/邮箱git config –global user.name [username]git config –global user.email [email]pull/fetch免密操作git config –global credential.helper store查看配置信息git config –list安装Webstorm和IDEA有了前面的经验,这次我直接曲软件库里找,果不其然,真的有不过IDEA是社区版,功能不全,想要专业版的只能曲官网下,手动配置图标启动,,这里就不赘述了

January 19, 2019 · 1 min · jiezi

如何将自己的前端代码,部署到搭建的nginx服务器上?

第一步: 下载 nginxnginx download官网地址下载后,将其解压到 本地的任一目录下。此时我们可以看到有如下目录:html路径下放置我们前端 build好的代码(如何build,相信各位都会),conf下有个非常重要的文件nginx.conf,用来配置nginx服务器。第二步: 配置nginx服务器打开nginx.conf文件,直接找到配置server 的地方,取消掉暂时用不到的配置,下面便是我的配置:server { # 启动后的端口 listen 8880; # 启动时的地址 server_name localhost; # 启动后,地址栏输入: localhost:8880, 默认会在html文件夹下找 index.html文件 location / { root html; index index.html; } # 404页面配置,页面同样在html文件夹中 error_page 404 /404.html; location = /404.html { root html; } # 其他错误码页面配置 error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } # 配置代理。由于项目是在本地起动的,而我们的request需要请求其他ip地址。如果你的request链接为localhost:8880/abc/login?name=12345,那么下面配的就是location /abc location /api { proxy_pass http://192.168.0.0:80; } # 一把前端不管用vue,还是react等框架,默认都是单页面的,如果你的项目是多页面的,则需要用到下面的配置。 # 因为此时你的浏览器的url不是localhost:8880/#/login,而是 localhost:8880/a.html/#/login # 所以我们需要将路径中a.html指向具体的html文件夹中的文件,因为默认是index.html location /a.html { alias html; index a.html; } location /b.html{ alias html; index b.html; }}第三步: 将build好的内容放到nginx下的html文件夹下只需要dist下的内容,如第四步: 启动nginx服务器在路径下右键,打开命令号工具,并输入>start nginx然后在浏览器地址栏输入localhost:8880即可第五步: 停止nginx服务器>nginx -s stop ...

January 18, 2019 · 1 min · jiezi

VUE-Router路由懒加载,打包问题(下午更改)

1、路由懒加载配置1.1index路由1.2home组件的路由1.3 添加路由导航守卫1.4项目打包1.4.1提示不能直接访问index.html文件,需要放在服务器中才可以访问。根据搜索解决办法,在 config > index.js 文件//assetsPublicPath: ‘/’, 添加.assetsPublicPath: ‘./’,重新打包后,仍然有提示1.4.2暂时不管,将文件放到虚拟机中继续访问,页面可以正常加载,但只要刷新页面就报404错误。后经过查询,需要将项目放在服务器中运行。不能直接访问静态页面。第二步,虚拟机安装nginx2、nginx的安装2.1配置完成后,启动项目报错 1067报错原因很多,一个个排查,因为IP冲突了。将IIS端口换成8082再启动成功访问locahost可以运行页面2.2将文件扔到nginx文件html文件中文件回退有问题,因为没有进行配置nginx.conf 文件配置配置完成后,再次刷新新页面还有问题2.3解决文件配置问题。因为打包前配置了 config > index.js 文件将配置路径还原,再次打包运行就没问题了。

January 18, 2019 · 1 min · jiezi

11 个 Nginx 参数性能优化工作

工作上,需要配置 Nginx,要投入生产使用,做了一点优化工作,加上以前也经常折腾 Nginx,故记下一些优化工作。优化 Nginx 进程数量配置参数如下:worker_processes 1; # 指定 Nginx 要开启的进程数,结尾的数字就是进程的个数,可以为 auto这个参数调整的是 Nginx 服务的 worker 进程数,Nginx 有 Master 进程和 worker 进程之分,Master 为管理进程、真正接待“顾客”的是 worker 进程。进程个数的策略:worker 进程数可以设置为等于 CPU 的核数。高流量高并发场合也可以考虑将进程数提高至 CPU 核数 x 2。这个参数除了要和 CPU 核数匹配之外,也与硬盘存储的数据及系统的负载有关,设置为 CPU 核数是个好的起始配置,也是官方建议的。当然,如果想省麻烦也可以配置为worker_processes auto;,将由 Nginx 自行决定 worker 数量。当访问量快速增加时,Nginx 就会临时 fork 新进程来缩短系统的瞬时开销和降低服务的时间。将不同的进程绑定到不同的CPU默认情况下,Nginx 的多个进程有可能运行在同一个 CPU 核上,导致 Nginx 进程使用硬件的资源不均,这就需要制定进程分配到指定的 CPU 核上处理,达到充分有效利用硬件的目的。配置参数如下:worker_processes 4;worker_cpu_affinity 0001 0010 0100 1000;其中 worker_cpu_affinity 就是配置 Nginx 进程与 CPU 亲和力的参数,即把不同的进程分给不同的 CPU 核处理。这里的0001 0010 0100 1000是掩码,分别代表第1、2、3、4核CPU。上述配置会为每个进程分配一核CPU处理。当然,如果想省麻烦也可以配置worker_cpu_affinity auto;,将由 Nginx 按需自动分配。Nginx 事件处理模型优化Nginx 的连接处理机制在不同的操作系统中会采用不同的 I/O 模型,在 linux 下,Nginx 使用 epoll 的 I/O 多路复用模型,在 Freebsd 中使用 kqueue 的 I/O 多路复用模型,在 Solaris 中使用 /dev/poll 方式的 I/O 多路复用模型,在 Windows 中使用 icop,等等。配置如下:events { use epoll;}events 指令是设定 Nginx 的工作模式及连接数上限。use指令用来指定 Nginx 的工作模式。Nginx 支持的工作模式有 select、 poll、 kqueue、 epoll 、 rtsig 和/ dev/poll。当然,也可以不指定事件处理模型,Nginx 会自动选择最佳的事件处理模型。单个进程允许的客户端最大连接数通过调整控制连接数的参数来调整 Nginx 单个进程允许的客户端最大连接数。events { worker_connections 20480;}worker_connections 也是个事件模块指令,用于定义 Nginx 每个进程的最大连接数,默认是 1024。最大连接数的计算公式如下:max_clients = worker_processes * worker_connections;如果作为反向代理,因为浏览器默认会开启 2 个连接到 server,而且 Nginx 还会使用fds(file descriptor)从同一个连接池建立连接到 upstream 后端。则最大连接数的计算公式如下:max_clients = worker_processes * worker_connections / 4;另外,进程的最大连接数受 Linux 系统进程的最大打开文件数限制,在执行操作系统命令 ulimit -HSn 65535或配置相应文件后, worker_connections 的设置才能生效。配置获取更多连接数默认情况下,Nginx 进程只会在一个时刻接收一个新的连接,我们可以配置multi_accept 为 on,实现在一个时刻内可以接收多个新的连接,提高处理效率。该参数默认是 off,建议开启。events { multi_accept on;}配置 worker 进程的最大打开文件数调整配置 Nginx worker 进程的最大打开文件数,这个控制连接数的参数为 worker_rlimit_nofile。该参数的实际配置如下:worker_rlimit_nofile 65535;可设置为系统优化后的 ulimit -HSn 的结果优化域名的散列表大小http { server_names_hash_bucket_size 128;}参数作用:设置存放域名( server names)的最大散列表的存储桶( bucket)的大小。 默认值依赖 CPU 的缓存行。server_names_hash_bucket_size 的值是不能带单位 的。配置主机时必须设置该值,否则无法运行 Nginx,或者无法通过测试 。 该设置与 server_ names_hash_max_size 共同控制保存服务器名的 hash 表, hash bucket size 总是等于 hash 表的大小, 并且是一路处理器缓存大小的倍数。若 hash bucket size 等于一路处理器缓存的大小,那么在查找键时, 最坏的情况下在内存中查找的次数为 2。第一次是确定存储单元的地址,第二次是在存储单元中查找键值 。 若报 出 hash max size 或 hash bucket size 的提示,则需要增加 server_names_hash_max size 的值。TCP 优化http { sendfile on; tcp_nopush on; keepalive_timeout 120; tcp_nodelay on;}第一行的 sendfile 配置可以提高 Nginx 静态资源托管效率。sendfile 是一个系统调用,直接在内核空间完成文件发送,不需要先 read 再 write,没有上下文切换开销。TCP_NOPUSH 是 FreeBSD 的一个 socket 选项,对应 Linux 的 TCP_CORK,Nginx 里统一用 tcp_nopush 来控制它,并且只有在启用了 sendfile 之后才生效。启用它之后,数据包会累计到一定大小之后才会发送,减小了额外开销,提高网络效率。TCP_NODELAY 也是一个 socket 选项,启用后会禁用 Nagle 算法,尽快发送数据,某些情况下可以节约 200ms(Nagle 算法原理是:在发出去的数据还未被确认之前,新生成的小数据先存起来,凑满一个 MSS 或者等到收到确认后再发送)。Nginx 只会针对处于 keep-alive 状态的 TCP 连接才会启用 tcp_nodelay。优化连接参数http { client_header_buffer_size 32k; large_client_header_buffers 4 32k; client_max_body_size 1024m; client_body_buffer_size 10m;}这部分更多是更具业务场景来决定的。例如client_max_body_size用来决定请求体的大小,用来限制上传文件的大小。上面列出的参数可以作为起始参数。配置压缩优化9.1、Gzip 压缩我们在上线前,代码(JS、CSS 和 HTML)会做压缩,图片也会做压缩(PNGOUT、Pngcrush、JpegOptim、Gifsicle 等)。对于文本文件,在服务端发送响应之前进行 GZip 压缩也很重要,通常压缩后的文本大小会减小到原来的 1/4 - 1/3。http { gzip on; gzip_buffers 16 8k; gzip_comp_level 6; gzip_http_version 1.0; gzip_min_length 1000; gzip_proxied any; gzip_vary on; gzip_types text/xml application/xml application/atom+xml application/rss+xml application/xhtml+xml image/svg+xml text/javascript application/javascript application/x-javascript text/x-json application/json application/x-web-app-manifest+json text/css text/plain text/x-component font/opentype application/x-font-ttf application/vnd.ms-fontobject image/x-icon; gzip_disable “MSIE [1-6].(?!.SV1)”;}这部分内容比较简单,只有两个地方需要解释下:gzip_vary 用来输出 Vary 响应头,用来解决某些缓存服务的一个问题,详情请看我之前的博客:HTTP 协议中 Vary 的一些研究。gzip_disable 指令接受一个正则表达式,当请求头中的 UserAgent 字段满足这个正则时,响应不会启用 GZip,这是为了解决在某些浏览器启用 GZip 带来的问题。默认 Nginx 只会针对 HTTP/1.1 及以上的请求才会启用 GZip,因为部分早期的 HTTP/1.0 客户端在处理 GZip 时有 Bug。现在基本上可以忽略这种情况,于是可以指定 gzip_http_version 1.0 来针对 HTTP/1.0 及以上的请求开启 GZip。9.2、Brotli 压缩Brotli 是基于LZ77算法的一个现代变体、霍夫曼编码和二阶上下文建模。Google软件工程师在2015年9月发布了包含通用无损数据压缩的Brotli增强版本,特别侧重于HTTP压缩。其中的编码器被部分改写以提高压缩比,编码器和解码器都提高了速度,流式API已被改进,增加更多压缩质量级别。需要安装libbrotli 、ngx_brotli ,重新编译 Nginx 时,带上–add-module=/path/to/ngx_brotli即可,然后配置如下http { brotli on; brotli_comp_level 6; brotli_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/javascript image/svg+xml;}Brotli 可与 Gzip 共存在一个配置文件中静态资源优化静态资源优化,可以减少连接请求数,同时也不需要对这些资源请求打印日志。但副作用是资源更新可能无法及时。server { # 图片、视频 location ~ ..(gif|jpg|jpeg|png|bmp|swf|flv|mp4|ico)$ { expires 30d; access_log off; } # 字体 location ~ ..(eot|ttf|otf|woff|svg)$ { expires 30d; access_log off; } # js、css location ~ ..(js|css)?$ { expires 7d; access_log off; }}收官~首发于 https://www.linpx.com ...

January 17, 2019 · 2 min · jiezi

Nginx配置Brotli压缩

在web应用中,为了节省流量,降低传输数据大小,提高传输效率,常用的压缩方式一般都是gzip,今天我们来介绍另外一种更高效的压缩方式brotli。 Brotli 是基于LZ77算法的一个现代变体、霍夫曼编码和二阶上下文建模。Google软件工程师在2015年9月发布了包含通用无损数据压缩的Brotli增强版本,特别侧重于HTTP压缩。其中的编码器被部分改写以提高压缩比,编码器和解码器都提高了速度,流式API已被改进,增加更多压缩质量级别。 与常见的通用压缩算法不同,Brotli使用一个预定义的120千字节字典。该字典包含超过13000个常用单词、短语和其他子字符串,这些来自一个文本和HTML文档的大型语料库。预定义的算法可以提升较小文件的压缩密度。使用Brotli替换Deflate(gzip)来对文本文件压缩通常可以增加20%的压缩密度,而压缩与解压缩速度则大致不变。浏览器支持情况Mozilla Firefox在Firefox 44中实现Brotli。Google Chrome从Chrome 49开始支持Brotli。Opera从Opera 36开始支持Brotli。以centos为例,配置Nginx使其支持brotli压缩1、安装依赖> yum groupinstall ‘Development Tools'2、安装libbrotlicd /usr/local/src/git clone https://github.com/bagder/libbrotlicd libbrotli./autogen.sh./configuremake && make install3、安装ngx_brotli> cd /usr/local/src/> git clone https://github.com/google/ngx_brotli> cd ngx_brotli && git submodule update –init4、下载Nginx> cd /usr/local/src> wget http://nginx.org/download/nginx-1.10.3.tar.gz> tar -xvzf nginx-1.10.3.tar.gz5、编译安装> cd /usr/local/src/nginx-1.10.3> ./configure –add-module=/usr/local/src/ngx_brotli> make# 如果是首次安装Nginx,执行make install;如果是升级,执行make upgrade> make install6、查看是否安装正常> nginx -V> nginx -t7、配置brotli在nginx.conf文件的http模块下新增以下内容#Brotli Compressionbrotli on;brotli_comp_level 6;brotli_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/javascript image/svg+xml;8、检查是否生效打开网页,用chrome开发者工具调试,在Network一栏会发现有content-encoding:br,同时网络耗时也会明显减少。欢迎订阅「K叔区块链」 - 专注于区块链技术学习 博客地址:http://www.jouypub.com简书主页:https://www.jianshu.com/u/756c9c8ae984segmentfault主页:https://segmentfault.com/blog/jouypub腾讯云主页:https://cloud.tencent.com/developer/column/72548

January 17, 2019 · 1 min · jiezi

使用rpmbuild制作Nginx的RPM包

前言题图为RPM包制作原理图,有时候为了方便源码包的安装,和我们自己订制软件包的需求,我们会把一些源码包按照我们的需求来做成 rpm 包,当有了源码包就可以直接编译得到二进制安装包和其他任意包。spec file 是制作 rpm 包最核心的部分,rpm 包的制作就是根据 spec file 来实现的。在制作自定义 rpm 包的时候最好不要使用管理员进行, 因为管理员权限过大,如果一个命令写错了,结果可能是灾难性的,而制件一个 rpm 包普通用户完全可以实现。本文主要介绍使用rpmbuild制作Nginx的RPM包,大部分步骤已经使用Bash Shell自动化完成了,大家可以基于此重新定义。使用rpmbuild制作Nginx的RPM包更新历史2019年01月16日 - 初稿阅读原文 - https://wsgzao.github.io/post…扩展阅读Creating RPM packages - https://docs.fedoraproject.or…How to create a GNU Hello RPM - https://fedoraproject.org/wik…使用 rpm-build 制作 nginx 的 rpm 包 - http://blog.51cto.com/nmshuis…什么是RPMAn RPM package is simply a file containing other files and information about them needed by the system. Specifically, an RPM package consists of the cpio archive, which contains the files, and the RPM header, which contains metadata about the package. The rpm package manager uses this metadata to determine dependencies, where to install files, and other information.There are two types of RPM packages:source RPM (SRPM)binary RPMSRPMs and binary RPMs share the file format and tooling, but have different contents and serve different purposes. An SRPM contains source code, optionally patches to it, and a SPEC file, which describes how to build the source code into a binary RPM. A binary RPM contains the binaries built from the sources and patches.RPM 有五种基本的操作功能:安装、卸载、升级、查询和验证。Linux 软件包分为两大类:二进制类包,包括 rpm 安装包(一般分为 i386 和 x86 等几种)源码类包,源码包和开发包应该归位此类(.src.rpm)在 Redhat 下,rpm 包的默认制作路径在 /usr/src/redhat 下,这其中包含了 6 个目录(要求全部大写)。但 Centos 并没有该目录,因此我们不得不自定义工作车间,即使在 Redhat 下有该目录,一般也是自定义到普通用户的家目录下的DirectoryUsageBUILD源代码解压以后放的位置,只需提供BUILD目录,具体里面放什么,不用我们管,所以真正的制作车间是BUILD目录RPMS制作完成后的rpm包存放目录,为特定平台指定子目录(i386,i686,ppc)SOURCES收集的源文件,源材料,补丁文件等存放位置SPECS存放spec文件,作为制作rpm包的领岗文件,以 rpm名.specSRPMSsrc格式的rpm包位置 ,既然是src格式的包,就没有平台的概念了BuiltRoot假根,使用install临时安装到这个目录,把这个目录当作根来用的,所以在这个目录下的目录文件,才是真正的目录文件。当打包完成后,在清理阶段,这个目录将被删除更详细的介绍可以参考 RPM Packaging Guidehttps://rpm-packaging-guide.g…制作 rpm 包如果你只关心如何使用可以直接跳过看下文,这里主要暂时代码和配置文件build shell# luajit.shLUAVER=2.0.5WKDIR="/root/rpmbuild/SOURCES"cd $WKDIRwget http://luajit.org/download/LuaJIT-$LUAVER.tar.gztar zxf LuaJIT-$LUAVER.tar.gzrm LuaJIT-$LUAVER.tar.gzcd LuaJIT-$LUAVERmake BUILDMODE=staticmake installexport LUAJIT_LIB=/usr/local/libexport LUAJIT_INC=/usr/local/include/luajit-2.0# build.shNGX_VER=1.14.1WKDIR="/root/rpmbuild/SOURCES"CURRENTDIR=dirname $(readlink -f "$0")echo $CURRENTDIRexport LUAJIT_LIB=/usr/local/libexport LUAJIT_INC=/usr/local/include/luajit-2.0cd $WKDIRwget http://nginx.org/download/nginx-$NGX_VER.tar.gztar xzf nginx-$NGX_VER.tar.gzrm nginx-$NGX_VER.tar.gzmv nginx-$NGX_VER nginx-garena-$NGX_VERcd nginx-garena-$NGX_VER/mkdir -p contribcd contrib/git clone git://github.com/bigplum/Nginx-limit-traffic-rate-module.gitgit clone git://github.com/agentzh/headers-more-nginx-module.git#git clone git://github.com/gnosek/nginx-upstream-fair.gitgit clone git://github.com/agentzh/echo-nginx-module.git#git clone git://github.com/arut/nginx-dav-ext-module.gitgit clone git://github.com/r10r/ngx_http_auth_pam_module.gitgit clone git://github.com/FRiCKLE/ngx_cache_purge.gitgit clone git://github.com/simpl/ngx_devel_kit.gitgit clone git://github.com/openresty/lua-nginx-module.gitgit clone git://github.com/nbs-system/naxsi.gitrm -rf /.gitcd ..cp -r $CURRENTDIR/nginx-template/ $WKDIR/nginx-garena-$NGX_VER/cp $CURRENTDIR/nginx-spec /root/rpmbuild/SPECS/#cp /root/rules $WKDIR/nginx-garena-$NGX_VER/debian/cd $WKDIRtar zcf nginx-garena-$NGX_VER.tar.gz nginx-garena-$NGX_VER/cd /root/rpmbuild/SPECS/rpmbuild -ba nginx-speccd /root/rpmbuild/RPMS/noarchnginx-spec# 1.The introduction section Name: nginx-garena # 软件包名称Version: 1.14.1 # 版本号Release: 0 # release号Summary: nginx garena rpm # 简要描述信息Source0: nginx-garena-1.14.1.tar.gz # source主要是引用一下自己定义好的脚本,配置文件之类的内容License: GPL # 一定带上(最好是对方源码包的License)BSD,GPL,GPLv2Group: Rahul # 要全用这里面的一个组:less /usr/share/doc/rpm-version/GROUPSBuildArch: noarch BuildRoot: %{_tmppath}/%{name}-buildroot %description # 软件包详述Garena self-build Nginx.%define _binaries_in_noarch_packages_terminate_build 0# 2.The Prep section 准备阶段,主要就是把源码包解压到build目录下,设置一下环境变量,并cd进去%prep%setup -q %{name}-%{version} # 这个宏的作用静默模式解压并cd# 3.The Build Section 编译制作阶段,这一节主要用于编译源码%buildCFLAGS="$RPM_OPT_FLAGS" ./configure –prefix=/usr/share/nginx/ \ –sbin-path=/usr/sbin/nginx \ –conf-path=/etc/nginx/nginx.conf \ –error-log-path=/var/log/nginx/error.log \ –http-log-path=/var/log/nginx/access.log \ –pid-path=/var/run/nginx.pid \ –lock-path=/var/lock/nginx.lock \ –http-client-body-temp-path=/var/lib/nginx/body \ –http-fastcgi-temp-path=/var/lib/nginx/fastcgi \ –http-proxy-temp-path=/var/lib/nginx/proxy \ –http-scgi-temp-path=/var/lib/nginx/scgi \ –http-uwsgi-temp-path=/var/lib/nginx/uwsgi \ –with-pcre-jit \ –with-http_flv_module \ –with-http_mp4_module \ –with-file-aio \ –with-http_v2_module \ –with-stream \ –with-stream_ssl_module \ –with-http_auth_request_module \ –with-http_slice_module \ –with-threads \ –with-http_gunzip_module \ –with-http_random_index_module \ –with-http_secure_link_module \ –with-http_geoip_module \ –with-http_ssl_module \ –with-openssl=/usr/local/src/openssl-1.0.2p \ –with-http_addition_module \ –with-http_geoip_module \ –with-http_gzip_static_module \ –with-http_realip_module \ –with-ipv6 \ –without-mail_pop3_module \ –without-mail_imap_module \ –without-mail_smtp_module \ –add-module=contrib/Nginx-limit-traffic-rate-module \ –add-module=contrib/headers-more-nginx-module \ –add-module=contrib/echo-nginx-module \ –add-module=contrib/ngx_http_auth_pam_module \ –add-module=contrib/ngx_cache_purge \ –add-module=contrib/ngx_devel_kit \ –add-module=contrib/lua-nginx-module \ –add-module=contrib/naxsi/naxsi_srcmake -j8# 4.Install section 这一节主要用于完成实际安装软件必须执行的命令,可包含4种类型脚本%install[ “$RPM_BUILD_ROOT” != “/” ] && rm -rf $RPM_BUILD_ROOTmake DESTDIR=$RPM_BUILD_ROOT installinstall -m 0755 -d $RPM_BUILD_ROOT/etc/nginx/sites-enabledinstall -m 0755 -d $RPM_BUILD_ROOT/etc/nginx/sites-availableinstall -m 0755 -d $RPM_BUILD_ROOT/var/log/nginxinstall -m 0755 -d $RPM_BUILD_ROOT/var/lib/nginxinstall -D -m 644 conf/sites-available/000_stub_status $RPM_BUILD_ROOT/etc/nginx/sites-available/000_stub_statusinstall -D -m 644 conf/django_fastcgi_params $RPM_BUILD_ROOT/etc/nginx/django_fastcgi_paramsinstall -D -m 644 conf/naxsi_core.rules $RPM_BUILD_ROOT/etc/nginx/naxsi_core.rulesinstall -D -m 644 conf/sites-available/000_stub_status $RPM_BUILD_ROOT/etc/nginx/sites-enabled/000_stub_statusinstall -D -m 644 logrotate.d/nginx $RPM_BUILD_ROOT/etc/logrotate.d/nginxinstall -D -m 644 nginx.service $RPM_BUILD_ROOT/usr/lib/systemd/system/nginx.service# 5.clean section 清理段,clean的主要作用就是删除BUILD%clean[ “$RPM_BUILD_ROOT” != “/” ] && rm -rf $RPM_BUILD_ROOT%postuseradd -s /sbin/nologin -d /var/www www-datachown -R www-data.www-data /var/log/nginx /var/lib/nginxsystemctl enable nginxecho %{name}-%{version} is successfully installed.systemctl start nginx# 6.file section 文件列表段,这个阶段是把前面已经编译好的内容要打包了%files%defattr(-,root,root)%dir /etc/nginx/etc/nginx/%dir /usr/src/debug/nginx-garena-1.14.1/usr/src/debug/nginx-garena-1.14.1//usr/sbin/nginx%dir /usr/share/nginx/usr/share/nginx//etc/logrotate.d/nginx/usr/lib/systemd/system/nginx.service/usr/lib/debug//usr/lib/debug/.build-id/%dir /var/log/nginx%dir /var/lib/nginx%config(noreplace) /etc/nginx/nginx.confnginx-templatenginx-template ├── conf │ ├── django_fastcgi_params │ ├── naxsi_core.rules │ └── sites-available │ └── 000_stub_status ├── logrotate.d │ └── nginx ├── nginx.conf └── nginx.service# nginx-rpmbuild-centos7/nginx-template/conf/django_fastcgi_paramsfastcgi_param QUERY_STRING $query_string;fastcgi_param REQUEST_METHOD $request_method;fastcgi_param CONTENT_TYPE $content_type;fastcgi_param CONTENT_LENGTH $content_length;fastcgi_param PATH_INFO $fastcgi_script_name;fastcgi_param REQUEST_URI $request_uri;fastcgi_param DOCUMENT_URI $document_uri;fastcgi_param DOCUMENT_ROOT $document_root;fastcgi_param SERVER_PROTOCOL $server_protocol;fastcgi_param GATEWAY_INTERFACE CGI/1.1;fastcgi_param SERVER_SOFTWARE nginx/$nginx_version;fastcgi_param REMOTE_ADDR $remote_addr;fastcgi_param REMOTE_PORT $remote_port;fastcgi_param SERVER_ADDR $server_addr;fastcgi_param SERVER_PORT $server_port;fastcgi_param SERVER_NAME $server_name;fastcgi_param HTTP_X_FORWARDED_PROTOCOL $scheme;fastcgi_pass_header Authorization;fastcgi_intercept_errors off;fastcgi_keep_conn on;# nginx-rpmbuild-centos7/nginx-template/conf/naxsi_core.rules#################################### INTERNAL RULES IDS:1-999 #####################################@MainRule “msg:weird request, unable to parse” id:1;#@MainRule “msg:request too big, stored on disk and not parsed” id:2;#@MainRule “msg:invalid hex encoding, null bytes” id:10;#@MainRule “msg:unknown content-type” id:11;#@MainRule “msg:invalid formatted url” id:12;#@MainRule “msg:invalid POST format” id:13;#@MainRule “msg:invalid POST boundary” id:14;#@MainRule “msg:invalid JSON” id:15;#@MainRule “msg:empty POST” id:16;#@MainRule “msg:libinjection_sql” id:17;#@MainRule “msg:libinjection_xss” id:18;#################################### SQL Injections IDs:1000-1099 ####################################MainRule “rx:select|union|update|delete|insert|table|from|ascii|hex|unhex|drop” “msg:sql keywords” “mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie” “s:$SQL:4” id:1000;MainRule “str:"” “msg:double quote” “mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie” “s:$SQL:8,$XSS:8” id:1001;MainRule “str:0x” “msg:0x, possible hex encoding” “mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie” “s:$SQL:2” id:1002;## Hardcore rulesMainRule “str:/” “msg:mysql comment (/)” “mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie” “s:$SQL:8” id:1003;MainRule “str:/” “msg:mysql comment (/)” “mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie” “s:$SQL:8” id:1004;MainRule “str:|” “msg:mysql keyword (|)” “mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie” “s:$SQL:8” id:1005;MainRule “str:&&” “msg:mysql keyword (&&)” “mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie” “s:$SQL:8” id:1006;## end of hardcore rulesMainRule “str:–” “msg:mysql comment (–)” “mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie” “s:$SQL:4” id:1007;MainRule “str:;” “msg:semicolon” “mz:BODY|URL|ARGS” “s:$SQL:4,$XSS:8” id:1008;MainRule “str:=” “msg:equal sign in var, probable sql/xss” “mz:ARGS|BODY” “s:$SQL:2” id:1009;MainRule “str:(” “msg:open parenthesis, probable sql/xss” “mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie” “s:$SQL:4,$XSS:8” id:1010;MainRule “str:)” “msg:close parenthesis, probable sql/xss” “mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie” “s:$SQL:4,$XSS:8” id:1011;MainRule “str:’” “msg:simple quote” “mz:ARGS|BODY|URL|$HEADERS_VAR:Cookie” “s:$SQL:4,$XSS:8” id:1013;MainRule “str:,” “msg:comma” “mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie” “s:$SQL:4” id:1015;MainRule “str:#” “msg:mysql comment (#)” “mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie” “s:$SQL:4” id:1016;MainRule “str:@@” “msg:double arobase (@@)” “mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie” “s:$SQL:4” id:1017;################################# OBVIOUS RFI IDs:1100-1199 #################################MainRule “str:http://” “msg:http:// scheme” “mz:ARGS|BODY|$HEADERS_VAR:Cookie” “s:$RFI:8” id:1100;MainRule “str:https://” “msg:https:// scheme” “mz:ARGS|BODY|$HEADERS_VAR:Cookie” “s:$RFI:8” id:1101;MainRule “str:ftp://” “msg:ftp:// scheme” “mz:ARGS|BODY|$HEADERS_VAR:Cookie” “s:$RFI:8” id:1102;MainRule “str:php://” “msg:php:// scheme” “mz:ARGS|BODY|$HEADERS_VAR:Cookie” “s:$RFI:8” id:1103;MainRule “str:sftp://” “msg:sftp:// scheme” “mz:ARGS|BODY|$HEADERS_VAR:Cookie” “s:$RFI:8” id:1104;MainRule “str:zlib://” “msg:zlib:// scheme” “mz:ARGS|BODY|$HEADERS_VAR:Cookie” “s:$RFI:8” id:1105;MainRule “str:data://” “msg:data:// scheme” “mz:ARGS|BODY|$HEADERS_VAR:Cookie” “s:$RFI:8” id:1106;MainRule “str:glob://” “msg:glob:// scheme” “mz:ARGS|BODY|$HEADERS_VAR:Cookie” “s:$RFI:8” id:1107;MainRule “str:phar://” “msg:phar:// scheme” “mz:ARGS|BODY|$HEADERS_VAR:Cookie” “s:$RFI:8” id:1108;MainRule “str:file://” “msg:file:// scheme” “mz:ARGS|BODY|$HEADERS_VAR:Cookie” “s:$RFI:8” id:1109;MainRule “str:gopher://” “msg:gopher:// scheme” “mz:ARGS|BODY|$HEADERS_VAR:Cookie” “s:$RFI:8” id:1110;######################################### Directory traversal IDs:1200-1299 #########################################MainRule “str:..” “msg:double dot” “mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie” “s:$TRAVERSAL:4” id:1200;MainRule “str:/etc/passwd” “msg:obvious probe” “mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie” “s:$TRAVERSAL:4” id:1202;MainRule “str:c:\” “msg:obvious windows path” “mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie” “s:$TRAVERSAL:4” id:1203;MainRule “str:cmd.exe” “msg:obvious probe” “mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie” “s:$TRAVERSAL:4” id:1204;MainRule “str:\” “msg:backslash” “mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie” “s:$TRAVERSAL:4” id:1205;#MainRule “str:/” “msg:slash in args” “mz:ARGS|BODY|$HEADERS_VAR:Cookie” “s:$TRAVERSAL:2” id:1206;########################################## Cross Site Scripting IDs:1300-1399 ##########################################MainRule “str:<” “msg:html open tag” “mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie” “s:$XSS:8” id:1302;MainRule “str:>” “msg:html close tag” “mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie” “s:$XSS:8” id:1303;MainRule “str:[” “msg:open square backet ([), possible js” “mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie” “s:$XSS:4” id:1310;MainRule “str:]” “msg:close square bracket (]), possible js” “mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie” “s:$XSS:4” id:1311;MainRule “str:” “msg:tilde () character” “mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie” “s:$XSS:4” id:1312;MainRule “str:" "msg:grave accent ()” “mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie” “s:$XSS:8” id:1314;MainRule “rx:%[2|3].” “msg:double encoding” “mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie” “s:$XSS:8” id:1315;###################################### Evading tricks IDs: 1400-1500 ######################################MainRule “str:&#” “msg:utf7/8 encoding” “mz:ARGS|BODY|URL|$HEADERS_VAR:Cookie” “s:$EVADE:4” id:1400;MainRule “str:%U” “msg:M$ encoding” “mz:ARGS|BODY|URL|$HEADERS_VAR:Cookie” “s:$EVADE:4” id:1401;############################### File uploads: 1500-1600 ###############################MainRule “rx:.ph|.asp|.ht” “msg:asp/php file upload” “mz:FILE_EXT” “s:$UPLOAD:8” id:1500;# nginx-rpmbuild-centos7/nginx-template/logrotate.d/nginx/var/log/nginx/.log /var/log/nginx//.log{ daily missingok rotate 14 compress delaycompress notifempty create 640 root adm sharedscripts postrotate [ ! -f /var/run/nginx.pid ] || kill -USR1 cat /var/run/nginx.pid endscript}# nginx-rpmbuild-centos7/nginx-template/nginx.confuser www-data;worker_processes auto;#worker_cpu_affinity 00000001 00000010 00000100 00001000 00010000 00100000 01000000 10000000;worker_rlimit_nofile 655650;error_log /var/log/nginx/error.log;pid /var/run/nginx.pid;events { worker_connections 10240;}http {# include /etc/nginx/naxsi_core.rules; include mime.types; default_type application/octet-stream; log_format garena ‘$remote_addr - $remote_user [$time_iso8601] “$request” $status $body_bytes_sent ’ ‘"$http_referer" “$http_user_agent” $request_time $upstream_response_time “$http_x_forwarded_for” “$geoip_country_code” “$host”’; log_format garena_post ‘$remote_addr - $remote_user [$time_iso8601] “$request” $status $body_bytes_sent ’ ‘"$http_referer" “$http_user_agent” $request_time $upstream_response_time “$http_x_forwarded_for” “$geoip_country_code” “$host” “$request_body”’; log_format compact ‘$time_iso8601|$remote_addr|$geoip_country_code|$http_x_forwarded_for|$status|$request_time|$upstream_response_time|$request_length|$body_bytes_sent|$host|$request|$http_referer|$http_user_agent’; log_format compact_post ‘$time_iso8601|$remote_addr|$geoip_country_code|$http_x_forwarded_for|$status|$request_time|$upstream_response_time|$request_length|$body_bytes_sent|$host|$request|$http_referer|$http_user_agent|$request_body’;# access_log logs/access.log main; sendfile on;# tcp_nopush on; keepalive_timeout 30; fastcgi_keep_conn on; tcp_nodelay on; gzip on; gzip_disable “MSIE [1-6].(?!.SV1)”; gzip_proxied any; gzip_buffers 16 8k; gzip_types text/plain application/javascript application/x-javascript text/javascript text/xml text/css application/json; gzip_vary on; include /etc/nginx/sites-enabled/; set_real_ip_from 10.0.0.0/8; real_ip_header X-Forwarded-For;# real_ip_recursive on;# geoip_country /usr/share/GeoIP/GeoIP.dat; server_tokens off; # returns “Server: nginx” more_clear_headers Server; # doesn’t return “Server: " header at all}# nginx-rpmbuild-centos7/nginx-template/nginx.service[Unit]Description=The nginx HTTP and reverse proxy serverAfter=network.target remote-fs.target nss-lookup.target[Service]Type=forkingPIDFile=/run/nginx.pidExecStartPre=/usr/sbin/nginx -tExecStart=/usr/sbin/nginxExecReload=/bin/kill -s HUP $MAINPIDKillMode=processKillSignal=SIGQUITTimeoutStopSec=5PrivateTmp=true[Install]WantedBy=multi-user.targetInitialize rpmbuild env# check current os version and kernelcat /etc/redhat-releaseCentOS Linux release 7.5.1804 (Core)uname -r3.10.0-862.el7.x86_64# install luash luajit.sh# yum install dependenciesyum install -y gcc pam-devel git rpm-build pcre-devel openssl openssl-devel geoip-devel# mkdirmkdir -p /root/rpmbuild/SOURCES/mkdir -p /root/rpmbuild/SPECS/mkdir -p /root/rpmbuild/RPMS/noarch# download opensslcd /usr/local/srcwget https://github.com/openssl/openssl/archive/OpenSSL_1_0_2p.tar.gztar xf OpenSSL_1_0_2p.tar.gzmv openssl-OpenSSL_1_0_2p/ openssl-1.0.2p# confirm these files are correct[root@localhost ~]# tree nginx-rpmbuild-centos7/nginx-rpmbuild-centos7/├── build.sh├── conf_buid│ ├── conf│ │ ├── django_fastcgi_params│ │ ├── fastcgi.conf│ │ ├── fastcgi_params│ │ ├── koi-utf│ │ ├── koi-win│ │ ├── mime.types│ │ ├── naxsi_core.rules│ │ ├── nginx.conf│ │ ├── scgi_params│ │ ├── sites-available│ │ │ └── 000_stub_status│ │ ├── uwsgi_params│ │ └── win-utf│ ├── logrotate.d│ │ └── nginx│ ├── nginx.conf│ └── nginx.service├── luajit.sh├── nginx-spec└── nginx-template ├── conf │ ├── django_fastcgi_params │ ├── naxsi_core.rules │ └── sites-available │ └── 000_stub_status ├── logrotate.d │ └── nginx ├── nginx.conf └── nginx.service8 directories, 24 filesHow to build Nginx RPM# check nginx stable version from official websitehttp://nginx.org/en/download.html# check configurationvim build.shNGX_VER=1.14.1WKDIR="/root/rpmbuild/SOURCES”# check nginx versionvim nginx-specreplace 1.14.1 to 1.14.2# run build.sh./build.sh# RPM packageProcessing files: nginx-garena-1.14.2-0.noarchwarning: File listed twice: /etc/nginx/nginx.confProvides: config(nginx-garena) = 1.14.2-0 nginx-garena = 1.14.2-0Requires(interp): /bin/shRequires(rpmlib): rpmlib(CompressedFileNames) <= 3.0.4-1 rpmlib(FileDigests) <= 4.6.0-1 rpmlib(PayloadFilesHavePrefix) <= 4.0-1Requires(post): /bin/shRequires: libGeoIP.so.1()(64bit) libc.so.6()(64bit) libc.so.6(GLIBC_2.10)(64bit) libc.so.6(GLIBC_2.11)(64bit) libc.so.6(GLIBC_2.14)(64bit) libc.so.6(GLIBC_2.17)(64bit) libc.so.6(GLIBC_2.2.5)(64bit) libc.so.6(GLIBC_2.3)(64bit) libc.so.6(GLIBC_2.3.2)(64bit) libc.so.6(GLIBC_2.3.4)(64bit) libc.so.6(GLIBC_2.4)(64bit) libc.so.6(GLIBC_2.7)(64bit) libcrypt.so.1()(64bit) libcrypt.so.1(GLIBC_2.2.5)(64bit) libdl.so.2()(64bit) libdl.so.2(GLIBC_2.2.5)(64bit) libgcc_s.so.1()(64bit) libgcc_s.so.1(GCC_3.0)(64bit) libgcc_s.so.1(GCC_3.3)(64bit) libm.so.6()(64bit) libm.so.6(GLIBC_2.2.5)(64bit) libpam.so.0()(64bit) libpam.so.0(LIBPAM_1.0)(64bit) libpcre.so.1()(64bit) libpthread.so.0()(64bit) libpthread.so.0(GLIBC_2.2.5)(64bit) libpthread.so.0(GLIBC_2.3.2)(64bit) libz.so.1()(64bit) rtld(GNU_HASH)warning: Arch dependent binaries in noarch packageChecking for unpackaged file(s): /usr/lib/rpm/check-files /root/rpmbuild/BUILDROOT/nginx-garena-1.14.2-0.x86_64Wrote: /root/rpmbuild/SRPMS/nginx-garena-1.14.2-0.src.rpmWrote: /root/rpmbuild/RPMS/noarch/nginx-garena-1.14.2-0.noarch.rpmExecuting(%clean): /bin/sh -e /var/tmp/rpm-tmp.iR5dLd+ umask 022+ cd /root/rpmbuild/BUILD+ cd nginx-garena-1.14.2+ ‘[’ /root/rpmbuild/BUILDROOT/nginx-garena-1.14.2-0.x86_64 ‘!=’ / ‘]’+ rm -rf /root/rpmbuild/BUILDROOT/nginx-garena-1.14.2-0.x86_64+ exit 0 ...

January 16, 2019 · 7 min · jiezi

nginx 详细配置例子

文件结构… #全局块events { #events块 …}http #http块{ … #http全局块 server #server块 { … #server全局块 location [PATTERN] #location块 { … } location [PATTERN] { … } } server { … } … #http全局块}例子#!nginx# 使用的用户和组,默认为nobody nobodyuser www www;# 指定工作衍生进程数,默认为1worker_processes 2;# 指定 pid 存放的路径pid /var/run/nginx.pid;# 制定日志路径,级别# 级别可以在下方直接使用 [ debug | info | notice | warn | error | crit ] 参数error_log /var/log/nginx.error_log info;events { # 允许的连接数 connections 2000; # use [ kqueue | rtsig | epoll | /dev/poll | select | poll ] ; # 具体内容查看 http://wiki.codemongers.com/事件模型 use kqueue;}http { # 文件扩展名与文件类型映射表 include conf/mime.types; # 文件扩展名与文件类型映射表 default_type application/octet-stream; # 自定义格式 main log_format main ‘$remote_addr - $remote_user [$time_local] ’ ‘"$request" $status $bytes_sent ’ ‘"$http_referer" “$http_user_agent” ’ ‘"$gzip_ratio"’; # 自定义格式 download log_format download ‘$remote_addr - $remote_user [$time_local] ’ ‘"$request" $status $bytes_sent ’ ‘"$http_referer" “$http_user_agent” ’ ‘"$http_range" “$sent_http_content_range”’; client_header_timeout 3m; client_body_timeout 3m; send_timeout 3m; client_header_buffer_size 1k; large_client_header_buffers 4 4k; gzip on; gzip_min_length 1100; gzip_buffers 4 8k; gzip_types text/plain; output_buffers 1 32k; postpone_output 1460; #允许sendfile方式传输文件,默认为off sendfile on; # 每个进程每次调用传输数量不能大于设定的值,默认为0,即不设上限。 # sendfile_max_chunk 100k; tcp_nopush on; tcp_nodelay on; send_lowat 12000; keepalive_timeout 75 20; # lingering_time 30; # lingering_timeout 10; # reset_timedout_connection on; server { # 监听端口 listen 80; # 域名可以有多个,用空格隔开 server_name one.example.com www.one.example.com; access_log /var/log/nginx.access_log main; # 日志格式, log_format main # 对 “/” 启用反向代理 location / { proxy_pass http://127.0.0.1:8001; proxy_redirect off; # 后端的Web服务器可以通过Host获取用户真实Host proxy_set_header Host $host; # 后端的Web服务器可以通过X-Real-IP获取用户真实remote_addr proxy_set_header X-Real-IP $remote_addr; # 后端的Web服务器可以通过X-Forwarded-For获取用户真实IP # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 允许客户端请求的最大单文件字节数 client_max_body_size 10m; # 缓冲区代理缓冲用户端请求的最大字节数 client_body_buffer_size 128k; client_body_temp_path /var/nginx/client_body_temp; # nginx跟后端服务器连接超时时间(代理连接超时) proxy_connect_timeout 90; # 后端服务器数据回传时间(代理发送超时) proxy_send_timeout 90; # 连接成功后,后端服务器响应时间(代理接收超时) proxy_read_timeout 90; proxy_send_lowat 12000; # 设置代理服务器(nginx)保存用户头信息的缓冲区大小 proxy_buffer_size 4k; # proxy_buffers缓冲区,网页平均在32k以下的设置 proxy_buffers 4 32k; # proxy_buffers缓冲区,网页平均在32k以下的设置 proxy_busy_buffers_size 64k; # 设定缓存文件夹大小,大于这个值,将从upstream服务器传 proxy_temp_file_write_size 64k; # 为存储承载从代理服务器接收到的数据的临时文件定义目录。指定目录下支持3级子目录结构 proxy_temp_path /var/nginx/proxy_temp; # 默认编码 charset utf-8; } error_page 404 /404.html; location /404.html { root /spool/www; charset on; source_charset koi8-r; } location /old_stuff/ { rewrite ^/old_stuff/(.)$ /new_stuff/$1 permanent; } location /download/ { valid_referers none blocked server_names .example.com; if ($invalid_referer) { #rewrite ^/ http://www.example.com/; return 403; } #rewrite_log on; # rewrite /download//mp3/.any_ext to /download//mp3/.mp3 rewrite ^/(download/.)/mp3/(.)..$ /$1/mp3/$2.mp3 break; root /spool/www; #autoindex on; access_log /var/log/nginx-download.access_log download; } location ~ ^.+.(jpg|jpeg|gif)$ { root /spool/www; access_log off; expires 30d; } }} ...

January 16, 2019 · 2 min · jiezi

一台阿里云主机 + one-sys脚手架 秒搭建自己的系统(博客、管理)

项目地址https://github.com/fanshyiis/…本脚手架主要致力于前端工程师的快速开发、一键部署等快捷开发框架,主要目的是想让前端工程师在一个阿里云服务器上可以快速开发部署自己的项目。本着前端后端融合统一的逻辑进行一些轮子的整合、并加入了自己的一些脚手架工具,第一次做脚手架的开发,如有问题,请在issue上提出,如果有帮助到您的地方,请不吝赐个star技术栈选择前端整合:vue-cli3.0、axios、element等命令行工具整合:commander、chalk、figlet、shelljs等后端整合:node、 koa2、koa-mysql-session、mysql等服务器整合:nginx、pm2、node等基本功能模块实现聚合分离所谓聚合分离,首先是‘聚合’,聚合代码,聚合插件,做到一个项目就可完成前端端代码的编写,打包上线等功能的聚合。其后是‘分离’。前后端分离。虽然代码会在同一个项目工程中但是前后端互不干扰,分别上线,区别于常规的ejs等服务端渲染的模式,做到前端完全分离一键部署基于本地的命令行工具,可以快速打包view端的静态文件并上传到阿里云服务器,也可快速上传server端的文件到服务器文件夹,配合pm2的监控功能进行代码的热更新,无缝更新接口逻辑快速迭代提供基本的使用案例,包括前端的view层的容器案例与组件案例,组件的api设定以及集合了axios的中间件逻辑,方便用户快速搭建自己的项目,代码清晰,易于分析与修改,server端对mysql连接池进行简单的封装,完成连接后及时释放,对table表格与函数进行分层,代码分层为路由层、控制器层、sql操作层基本模块举例1.登录页面登录 -正确反馈 错误反馈 登录成功后session的设定注册 -重名检测 正确反馈 错误反馈主要模块功能模块增删查改基本功能的实现后台koa2服务模块配合koa-mysql-session进行session的设定储存checkLogin中间件的实现cors跨域白名单的设定middlewer 中间件的设定mysql连接池的封装等等。。。服务端nginx 的基本配置与前端端分离的配置pm2 多实例构建配置文件的配置文件 pm2config.json使用流程本地调试安装mysql (过程请百度)// 进入sql命令行$ mysql -u root -p// 创建名为nodesql的数据库$ create database nodesql安装pm2 (过程请百度)拉取项目代码git clone https://github.com/fanshyiis/ONE-syscd ONE-sys// 安装插件cnpm i 或 npm i 或者 yarn add// 安装linksudo npm link// 然后就能使用命令行工具了one start// 或者不愿意使用命令行的同学可以yarn run serve主要代码解析代码逻辑serverbinone -h启动效果启动项目yarn run v1.3.2$ pm2 restart ./server/index.js && vue-cli-service serveUse –update-env to update environment variables[PM2] Applying action restartProcessId on app [./server/index.js](ids: 0,1)[PM2] index ✓[PM2] one-sys ✓┌──────────┬────┬─────────┬─────────┬───────┬────────┬─────────┬────────┬─────┬───────────┬───────────┬──────────┐│ App name │ id │ version │ mode │ pid │ status │ restart │ uptime │ cpu │ mem │ user │ watching │├──────────┼────┼─────────┼─────────┼───────┼────────┼─────────┼────────┼─────┼───────────┼───────────┼──────────┤│ index │ 0 │ 0.1.0 │ fork │ 77439 │ online │ 2640 │ 0s │ 0% │ 15.4 MB │ koala_cpx │ disabled ││ one-sys │ 1 │ 0.1.0 │ cluster │ 77438 │ online │ 15 │ 0s │ 0% │ 20.2 MB │ koala_cpx │ disabled │└──────────┴────┴─────────┴─────────┴───────┴────────┴─────────┴────────┴─────┴───────────┴───────────┴──────────┘ Use pm2 show &lt;id|name&gt; to get more details about an app INFO Starting development server… 98% after emitting CopyPlugin DONE Compiled successfully in 10294ms16:31:55 App running at: - Local: http://localhost:8080/ - Network: http://192.168.7.69:8080/ Note that the development build is not optimized. To create a production build, run yarn build.页面展示线上调试阿里云服务器文件存放目录[root@iZm5e6naugml8q0362d8rfZ ]# cd /home/[root@iZm5e6naugml8q0362d8rfZ home]# lsdist server test[root@iZm5e6naugml8q0362d8rfZ home]#阿里云nginx配置 location ^ /api { proxy_pass http://127.0.0.1:3000; } location ^ /redAlert/ { root /home/dist/; try_files $uri $uri/ /index.html =404; } location ^~ /file/ { alias /home/server/controller/public/; } location / { root /home/dist/; index index.html index.htm; }其他方面如同本地配置有问题可以加群联系最后请star一个吧~~~ ...

January 15, 2019 · 2 min · jiezi

Nginx服务器添加密码验证

有时侯部分网站不想被别人看到,需要进行加密访问,此时对nginx进行简单配置即可nginx域名配置文件修改location / { root html; index index.html index.htm index index.jpg; auth_basic ‘Restricted’; # 认证名称,随意填写 auth_basic_user_file /htpasswd/passwd.db; # 认证的密码文件,需要生产。 }通过htpasswd命令生成用户名及对应密码数据库文件htpasswd -c /htpasswd/passwd.db testchmod 777 /htpasswd/passwd.db test/usr/local/nginx/sbin -s relaod

January 15, 2019 · 1 min · jiezi

Android开发基于rtmp实现视频直播

前言近两年时间,视频直播可谓大火。在视频直播领域,有不同的商家提供各种的商业解决方案,包括软硬件设备,摄像机,编码器,流媒体服务器等。本文要讲解的是如何使用一系列免费工具,打造一套视频直播方案。视频直播流程视频直播的流程可以分为如下几步:采集 —>处理—>编码和封装—>推流到服务器—>服务器流分发—>播放器流播放一般情况下我们把流程的前四步称为第一部分,即视频主播端的操作。视频采集处理后推流到流媒体服务器,第一部分功能完成。第二部分就是流媒体服务器,负责把从第一部分接收到的流进行处理并分发给观众。第三部分就是观众啦,只需要拥有支持流传输协议的播放器即可。一、采集采集是整个视频推流过程中的第一个环节,它从系统的采集设备中获取原始视频数据,将其输出到下一个环节。视频的采集涉及两方面数据的采集:音频采集和图像采集,它们分别对应两种完全不同的输入源和数据格式。1.1-音频采集音频数据既能与图像结合组合成视频数据,也能以纯音频的方式采集播放,后者在很多成熟的应用场景如在线电台和语音电台等起着非常重要的作用。音频的采集过程主要通过设备将环境中的模拟信号采集成 PCM 编码的原始数据,然后编码压缩成 MP3 等格式的数据分发出去。常见的音频压缩格式有:MP3,AAC,HE-AAC,Opus,FLAC,Vorbis (Ogg),Speex 和 AMR等。音频采集和编码主要面临的挑战在于:延时敏感、卡顿敏感、噪声消除(Denoise)、回声消除(AEC)、静音检测(VAD)和各种混音算法等。1.2-图像采集将图像采集的图片结果组合成一组连续播放的动画,即构成视频中可肉眼观看的内容。图像的采集过程主要由摄像头等设备拍摄成 YUV 编码的原始数据,然后经过编码压缩成 H.264 等格式的数据分发出去。常见的视频封装格式有:MP4、3GP、AVI、MKV、WMV、MPG、VOB、FLV、SWF、MOV、RMVB 和 WebM 等。图像由于其直观感受最强并且体积也比较大,构成了一个视频内容的主要部分。图像采集和编码面临的主要挑战在于:设备兼容性差、延时敏感、卡顿敏感以及各种对图像的处理操作如美颜和水印等。视频采集的采集源主要有 摄像头采集、屏幕录制和从视频文件推流。二、处理视频或者音频完成采集之后得到原始数据,为了增强一些现场效果或者加上一些额外的效果,我们一般会在将其编码压缩前进行处理,比如打上时间戳或者公司 Logo 的水印,祛斑美颜和声音混淆等处理。在主播和观众连麦场景中,主播需要和某个或者多个观众进行对话,并将对话结果实时分享给其他所有观众,连麦的处理也有部分工作在推流端完成。如上图所示,处理环节中分为音频和视频处理,音频处理中具体包含混音、降噪和声音特效等处理,视频处理中包含美颜、水印、以及各种自定义滤镜等处理。三、编码和封装3.1-编码如果把整个流媒体比喻成一个物流系统,那么编解码就是其中配货和装货的过程,这个过程非常重要,它的速度和压缩比对物流系统的意义非常大,影响物流系统的整体速度和成本。同样,对流媒体传输来说,编码也非常重要,它的编码性能、编码速度和编码压缩比会直接影响整个流媒体传输的用户体验和传输成本。视频编码的意义原始视频数据存储空间大,一个 1080P 的 7 s 视频需要 817 MB原始视频数据传输占用带宽大,10 Mbps 的带宽传输上述 7 s 视频需要 11 分钟而经过 H.264 编码压缩之后,视频大小只有 708 k ,10 Mbps 的带宽仅仅需要 500 ms 可以满足实时传输的需求,所以从视频采集传感器采集来的原始视频势必要经过视频编码。⑴.基本原理为什么巨大的原始视频可以编码成很小的视频呢?这其中的技术是什么呢?核心思想就是去除冗余信息:1、空间冗余:图像相邻像素之间有较强的相关性2、时间冗余:视频序列的相邻图像之间内容相似3、编码冗余:不同像素值出现的概率不同4、视觉冗余:人的视觉系统对某些细节不敏感5、知识冗余:规律性的结构可由先验知识和背景知识得到⑵.编码器的选择视频编码器经历了数十年的发展,已经从开始的只支持帧内编码演进到现如今的 H.265和 VP9 为代表的新一代编码器,下面是一些常见的视频编码器:1.H.264/AVC2.HEVC/H.2653.VP84.VP95.FFmpeg注:音频编码器有Mp3, AAC等。3.2-封装沿用前面的比喻,封装可以理解为采用哪种货车去运输,也就是媒体的容器。所谓容器,就是把编码器生成的多媒体内容(视频,音频,字幕,章节信息等)混合封装在一起的标准。容器使得不同多媒体内容同步播放变得很简单,而容器的另一个作用就是为多媒体内容提供索引,也就是说如果没有容器存在的话一部影片你只能从一开始看到最后,不能拖动进度条,而且如果你不自己去手动另外载入音频就没有声音。下面是几种常见的封装格式:1.AVI 格式(后缀为 .avi)2.DV-AVI 格式(后缀为 .avi)3.QuickTime File Format 格式(后缀为 .mov)4.MPEG 格式(文件后缀可以是 .mpg .mpeg .mpe .dat .vob .asf .3gp .mp4等)5.WMV 格式(后缀为.wmv .asf)6.Real Video 格式(后缀为 .rm .rmvb)7.Flash Video 格式(后缀为 .flv)8.Matroska 格式(后缀为 .mkv)9.MPEG2-TS 格式 (后缀为 .ts)目前,我们在流媒体传输,尤其是直播中主要采用的就是 FLV 和 MPEG2-TS 格式,分别用于 RTMP/HTTP-FLV 和 HLS协议。四、推流到服务器推流是直播的第一公里,直播的推流对这个直播链路影响非常大,如果推流的网络不稳定,无论我们如何做优化,观众的体验都会很糟糕。所以也是我们排查问题的第一步,如何系统地解决这类问题需要我们对相关理论有基础的认识。推送协议主要有三种:RTSP(Real Time Streaming Protocol):实时流传送协议,是用来控制声音或影像的多媒体串流协议, 由Real Networks和Netscape共同提出的;RTMP(Real Time Messaging Protocol):实时消息传送协议,是Adobe公司为Flash播放器和服务器之间音频、视频和数据传输 开发的开放协议;HLS(HTTP Live Streaming):是苹果公司(Apple Inc.)实现的基于HTTP的流媒体传输协议;RTMP协议基于 TCP,是一种设计用来进行实时数据通信的网络协议,主要用来在 flash/AIR 平台和支持 RTMP 协议的流媒体/交互服务器之间进行音视频和数据通信。支持该协议的软件包括 Adobe Media Server/Ultrant Media Server/red5 等。它有三种变种:RTMP工作在TCP之上的明文协议,使用端口1935;RTMPT封装在HTTP请求之中,可穿越防火墙;RTMPS类似RTMPT,但使用的是HTTPS连接;RTMP 是目前主流的流媒体传输协议,广泛用于直播领域,可以说市面上绝大多数的直播产品都采用了这个协议。RTMP协议就像一个用来装数据包的容器,这些数据可以是AMF格式的数据,也可以是FLV中的视/音频数据。一个单一的连接可以通过不同的通道传输多路网络流。这些通道中的包都是按照固定大小的包传输的。五、服务器流分发流媒体服务器的作用是负责直播流的发布和转播分发功能。流媒体服务器有诸多选择,如商业版的Wowza。但我选择的是Nginx,它是一款优秀的免费Web服务器,后面我会详细介绍如何搭建Nginx服务器。六、播放器流播放主要是实现直播节目在终端上的展现。因为我这里使用的传输协议是RTMP, 所以只要支持 RTMP 流协议的播放器都可以使用,譬如:电脑端:VLC等手机端:Vitamio以及ijkplayer等第一部分:采集推流SDK目前市面上集视频采集、编码、封装和推流于一体的SDK已经有很多了,例如商业版的NodeMedia,但NodeMedia SDK按包名授权,未授权包名应用使用有版权提示信息。我这里使用的是别人分享在github上的一个免费SDK。文章下点赞+私信我获取!下面我就代码分析一下直播推流的过程吧:先看入口界面:很简单,一个输入框让你填写服务器的推流地址,另外一个按钮开启推流。public class StartActivity extends Activity { public static final String RTMPURL_MESSAGE = “rtmppush.hx.com.rtmppush.rtmpurl”; private Button _startRtmpPushButton = null; private EditText _rtmpUrlEditText = null; private View.OnClickListener _startRtmpPushOnClickedEvent = new View.OnClickListener() { @Override public void onClick(View arg0) { Intent i = new Intent(StartActivity.this, MainActivity.class); String rtmpUrl = _rtmpUrlEditText.getText().toString(); i.putExtra(StartActivity.RTMPURL_MESSAGE, rtmpUrl); StartActivity.this.startActivity(i); } }; private void InitUI(){ _rtmpUrlEditText = (EditText)findViewById(R.id.rtmpUrleditText); _startRtmpPushButton = (Button)findViewById(R.id.startRtmpButton); _rtmpUrlEditText.setText(“rtmp://192.168.1.104:1935/live/12345”); _startRtmpPushButton.setOnClickListener(_startRtmpPushOnClickedEvent); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_start); InitUI(); }}主要的推流过程在MainActivity里面,同样,先看界面: 布局文件:<RelativeLayout xmlns:android=“http://schemas.android.com/apk/res/android" xmlns:tools=“http://schemas.android.com/tools" android:id=”@+id/cameraRelative” android:layout_width=“match_parent” android:layout_height=“match_parent” android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" android:theme="@android:style/Theme.NoTitleBar.Fullscreen"><SurfaceView android:id="@+id/surfaceViewEx" android:layout_width=“match_parent” android:layout_height=“match_parent”/> <Button android:id="@+id/SwitchCamerabutton" android:layout_width=“wrap_content” android:layout_height=“wrap_content” android:layout_alignBottom="@+id/surfaceViewEx" android:text="@string/SwitchCamera" /></RelativeLayout>其实就是用一个SurfaceView显示摄像头拍摄画面,并提供了一个按钮切换前置和后置摄像头。从入口函数看起: @Override protected void onCreate(Bundle savedInstanceState) { requestWindowFeature(Window.FEATURE_NO_TITLE); getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); Intent intent = getIntent(); _rtmpUrl = intent.getStringExtra(StartActivity.RTMPURL_MESSAGE); InitAll(); PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); _wakeLock = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, “My Tag”); }首先设置全屏显示,常亮,竖屏,获取服务器的推流url,再初始化所有东西。 private void InitAll() { WindowManager wm = this.getWindowManager(); int width = wm.getDefaultDisplay().getWidth(); int height = wm.getDefaultDisplay().getHeight(); int iNewWidth = (int) (height * 3.0 / 4.0); RelativeLayout rCameraLayout = (RelativeLayout) findViewById(R.id.cameraRelative); RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT); int iPos = width - iNewWidth; layoutParams.setMargins(iPos, 0, 0, 0); _mSurfaceView = (SurfaceView) this.findViewById(R.id.surfaceViewEx); _mSurfaceView.getHolder().setFixedSize(HEIGHT_DEF, WIDTH_DEF); _mSurfaceView.getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); _mSurfaceView.getHolder().setKeepScreenOn(true); _mSurfaceView.getHolder().addCallback(new SurceCallBack()); _mSurfaceView.setLayoutParams(layoutParams); InitAudioRecord(); _SwitchCameraBtn = (Button) findViewById(R.id.SwitchCamerabutton); _SwitchCameraBtn.setOnClickListener(_switchCameraOnClickedEvent); RtmpStartMessage();//开始推流 }首先设置屏幕比例3:4显示,给SurfaceView设置一些参数并添加回调,再初始化AudioRecord,最后执行开始推流。音频在这里初始化了,那么相机在哪里初始化呢?其实在SurfaceView的回调函数里。 @Override public void surfaceCreated(SurfaceHolder holder) { _iDegrees = getDisplayOritation(getDispalyRotation(), 0); if (_mCamera != null) { InitCamera(); //初始化相机 return; } //华为i7前后共用摄像头 if (Camera.getNumberOfCameras() == 1) { _bIsFront = false; _mCamera = Camera.open(Camera.CameraInfo.CAMERA_FACING_BACK); } else { _mCamera = Camera.open(Camera.CameraInfo.CAMERA_FACING_FRONT); } InitCamera(); } @Override public void surfaceDestroyed(SurfaceHolder holder) { } }相机的初始化就在这里啦: public void InitCamera() { Camera.Parameters p = _mCamera.getParameters(); Size prevewSize = p.getPreviewSize(); showlog(“Original Width:” + prevewSize.width + “, height:” + prevewSize.height); List<Size> PreviewSizeList = p.getSupportedPreviewSizes(); List<Integer> PreviewFormats = p.getSupportedPreviewFormats(); showlog(“Listing all supported preview sizes”); for (Camera.Size size : PreviewSizeList) { showlog(" w: " + size.width + “, h: " + size.height); } showlog(“Listing all supported preview formats”); Integer iNV21Flag = 0; Integer iYV12Flag = 0; for (Integer yuvFormat : PreviewFormats) { showlog(“preview formats:” + yuvFormat); if (yuvFormat == android.graphics.ImageFormat.YV12) { iYV12Flag = android.graphics.ImageFormat.YV12; } if (yuvFormat == android.graphics.ImageFormat.NV21) { iNV21Flag = android.graphics.ImageFormat.NV21; } } if (iNV21Flag != 0) { _iCameraCodecType = iNV21Flag; } else if (iYV12Flag != 0) { _iCameraCodecType = iYV12Flag; } p.setPreviewSize(HEIGHT_DEF, WIDTH_DEF); p.setPreviewFormat(_iCameraCodecType); p.setPreviewFrameRate(FRAMERATE_DEF); showlog("_iDegrees="+_iDegrees); _mCamera.setDisplayOrientation(_iDegrees); p.setRotation(_iDegrees); _mCamera.setPreviewCallback(_previewCallback); _mCamera.setParameters(p); try { _mCamera.setPreviewDisplay(_mSurfaceView.getHolder()); } catch (Exception e) { return; } _mCamera.cancelAutoFocus();//只有加上了这一句,才会自动对焦。 _mCamera.startPreview(); }还记得之前初始化完成之后开始推流函数吗? private void RtmpStartMessage() { Message msg = new Message(); msg.what = ID_RTMP_PUSH_START; Bundle b = new Bundle(); b.putInt(“ret”, 0); msg.setData(b); mHandler.sendMessage(msg); }Handler处理: public Handler mHandler = new Handler() { public void handleMessage(android.os.Message msg) { Bundle b = msg.getData(); int ret; switch (msg.what) { case ID_RTMP_PUSH_START: { Start(); break; } } } };真正的推流实现原来在这里: private void Start() { if (DEBUG_ENABLE) { File saveDir = Environment.getExternalStorageDirectory(); String strFilename = saveDir + “/aaa.h264”; try { if (!new File(strFilename).exists()) { new File(strFilename).createNewFile(); } _outputStream = new DataOutputStream(new FileOutputStream(strFilename)); } catch (Exception e) { e.printStackTrace(); } } //_rtmpSessionMgr.Start(“rtmp://192.168.0.110/live/12345678”); _rtmpSessionMgr = new RtmpSessionManager(); _rtmpSessionMgr.Start(_rtmpUrl); //——point 1 int iFormat = _iCameraCodecType; _swEncH264 = new SWVideoEncoder(WIDTH_DEF, HEIGHT_DEF, FRAMERATE_DEF, BITRATE_DEF); _swEncH264.start(iFormat); //——point 2 _bStartFlag = true; _h264EncoderThread = new Thread(_h264Runnable); _h264EncoderThread.setPriority(Thread.MAX_PRIORITY); _h264EncoderThread.start(); //——point 3 _AudioRecorder.startRecording(); _AacEncoderThread = new Thread(_aacEncoderRunnable); _AacEncoderThread.setPriority(Thread.MAX_PRIORITY); _AacEncoderThread.start(); //——point 4 }里面主要的函数有四个,我分别标出来了,现在我们逐一看一下。首先是point 1,这已经走到SDK里面了 public int Start(String rtmpUrl){ int iRet = 0; _rtmpUrl = rtmpUrl; _rtmpSession = new RtmpSession(); _bStartFlag = true; _h264EncoderThread.setPriority(Thread.MAX_PRIORITY); _h264EncoderThread.start(); return iRet; }其实就是启动了一个线程,这个线程稍微有点复杂 private Thread _h264EncoderThread = new Thread(new Runnable() { private Boolean WaitforReConnect(){ for(int i=0; i < 500; i++){ try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } if(_h264EncoderThread.interrupted() || (!_bStartFlag)){ return false; } } return true; } @Override public void run() { while (!_h264EncoderThread.interrupted() && (_bStartFlag)) { if(_rtmpHandle == 0) { _rtmpHandle = _rtmpSession.RtmpConnect(_rtmpUrl); if(_rtmpHandle == 0){ if(!WaitforReConnect()){ break; } continue; } }else{ if(_rtmpSession.RtmpIsConnect(_rtmpHandle) == 0){ _rtmpHandle = _rtmpSession.RtmpConnect(_rtmpUrl); if(_rtmpHandle == 0){ if(!WaitforReConnect()){ break; } continue; } } } if((_videoDataQueue.size() == 0) && (_audioDataQueue.size()==0)){ try { Thread.sleep(30); } catch (InterruptedException e) { e.printStackTrace(); } continue; } //Log.i(TAG, “VideoQueue length="+_videoDataQueue.size()+”, AudioQueue length="+_audioDataQueue.size()); for(int i = 0; i < 100; i++){ byte[] audioData = GetAndReleaseAudioQueue(); if(audioData == null){ break; } //Log.i(TAG, “###RtmpSendAudioData:"+audioData.length); _rtmpSession.RtmpSendAudioData(_rtmpHandle, audioData, audioData.length); } byte[] videoData = GetAndReleaseVideoQueue(); if(videoData != null){ //Log.i(TAG, “$$$RtmpSendVideoData:"+videoData.length); _rtmpSession.RtmpSendVideoData(_rtmpHandle, videoData, videoData.length); } try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } _videoDataQueueLock.lock(); _videoDataQueue.clear(); _videoDataQueueLock.unlock(); _audioDataQueueLock.lock(); _audioDataQueue.clear(); _audioDataQueueLock.unlock(); if((_rtmpHandle != 0) && (_rtmpSession != null)){ _rtmpSession.RtmpDisconnect(_rtmpHandle); } _rtmpHandle = 0; _rtmpSession = null; } });看18行,主要就是一个while循环,每隔一段时间去_audioDataQueue和_videoDataQueue两个缓冲数组中取数据发送给服务器,发送方法_rtmpSession.RtmpSendAudioData和_rtmpSession.RtmpSendVideoData都是Native方法,通过jni调用so库文件的内容,每隔一段时间,这个时间是多少呢?看第4行,原来是5秒钟,也就是说我们的视频数据会在缓冲中存放5秒才被取出来发给服务器,所有直播会有5秒的延时,我们可以修改这块来控制直播延时。 上面说了我们会从_audioDataQueue和_videoDataQueue两个Buffer里面取数据,那么数据是何时放进去的呢?看上面的point 2,3,4。首先是point 2,同样走进了SDK: public boolean start(int iFormateType){ int iType = OpenH264Encoder.YUV420_TYPE; if(iFormateType == android.graphics.ImageFormat.YV12){ iType = OpenH264Encoder.YUV12_TYPE; }else{ iType = OpenH264Encoder.YUV420_TYPE; } _OpenH264Encoder = new OpenH264Encoder(); _iHandle = _OpenH264Encoder.InitEncode(_iWidth, _iHeight, _iBitRate, _iFrameRate, iType); if(_iHandle == 0){ return false; } _iFormatType = iFormateType; return true; }其实这是初始化编码器,具体的初始化过程也在so文件,jni调用。point 3,4其实就是开启两个线程,那我们看看线程中具体实现吧。 private Thread _h264EncoderThread = null; private Runnable _h264Runnable = new Runnable() { @Override public void run() { while (!_h264EncoderThread.interrupted() && _bStartFlag) { int iSize = _YUVQueue.size(); if (iSize > 0) { _yuvQueueLock.lock(); byte[] yuvData = _YUVQueue.poll(); if (iSize > 9) { Log.i(LOG_TAG, “###YUV Queue len=” + _YUVQueue.size() + “, YUV length=” + yuvData.length); } _yuvQueueLock.unlock(); if (yuvData == null) { continue; } if (_bIsFront) { _yuvEdit = _swEncH264.YUV420pRotate270(yuvData, HEIGHT_DEF, WIDTH_DEF); } else { _yuvEdit = _swEncH264.YUV420pRotate90(yuvData, HEIGHT_DEF, WIDTH_DEF); } byte[] h264Data = _swEncH264.EncoderH264(_yuvEdit); if (h264Data != null) { _rtmpSessionMgr.InsertVideoData(h264Data); if (DEBUG_ENABLE) { try { _outputStream.write(h264Data); int iH264Len = h264Data.length; //Log.i(LOG_TAG, “Encode H264 len="+iH264Len); } catch (IOException e1) { e1.printStackTrace(); } } } } try { Thread.sleep(1); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } _YUVQueue.clear(); } };也是一个循环线程,第9行,从_YUVQueue中取出摄像头获取的数据,然后进行视频旋转,第24行,对数据进行编码,然后执行26行,InsertVideoData: public void InsertVideoData(byte[] videoData){ if(!_bStartFlag){ return; } _videoDataQueueLock.lock(); if(_videoDataQueue.size() > 50){ _videoDataQueue.clear(); } _videoDataQueue.offer(videoData); _videoDataQueueLock.unlock(); }果然就是插入之前提到的_videoDataQueue的Buffer。这里插入的是视频数据,那么音频数据呢?在另外一个线程,内容大致相同private Runnable _aacEncoderRunnable = new Runnable() { @Override public void run() { DataOutputStream outputStream = null; if (DEBUG_ENABLE) { File saveDir = Environment.getExternalStorageDirectory(); String strFilename = saveDir + “/aaa.aac”; try { if (!new File(strFilename).exists()) { new File(strFilename).createNewFile(); } outputStream = new DataOutputStream(new FileOutputStream(strFilename)); } catch (Exception e1) { e1.printStackTrace(); } } long lSleepTime = SAMPLE_RATE_DEF * 16 * 2 / _RecorderBuffer.length; while (!_AacEncoderThread.interrupted() && _bStartFlag) { int iPCMLen = _AudioRecorder.read(_RecorderBuffer, 0, _RecorderBuffer.length); // Fill buffer if ((iPCMLen != _AudioRecorder.ERROR_BAD_VALUE) && (iPCMLen != 0)) { if (_fdkaacHandle != 0) { byte[] aacBuffer = _fdkaacEnc.FdkAacEncode(_fdkaacHandle, _RecorderBuffer); if (aacBuffer != null) { long lLen = aacBuffer.length; _rtmpSessionMgr.InsertAudioData(aacBuffer); //Log.i(LOG_TAG, “fdk aac length="+lLen+” from pcm="+iPCMLen); if (DEBUG_ENABLE) { try { outputStream.write(aacBuffer); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } } else { Log.i(LOG_TAG, “######fail to get PCM data”); } try { Thread.sleep(lSleepTime / 10); } catch (InterruptedException e) { e.printStackTrace(); } } Log.i(LOG_TAG, “AAC Encoder Thread ended ……”); } }; private Thread _AacEncoderThread = null;这就是通过循环将音频数据插入_audioDataQueue这个Buffer。以上就是视频采集和推流的代码分析,Demo中并没有对视频进行任何处理,只是摄像头采集,编码后推流到服务器端。第二部分:Nginx服务器搭建流媒体服务器有诸多选择,如商业版的Wowza。但我选择的是免费的Nginx(nginx-rtmp-module)。Nginx本身是一个非常出色的HTTP服务器,它通过nginx的模块nginx-rtmp-module可以搭建一个功能相对比较完善的流媒体服务器。这个流媒体服务器可以支持RTMP和HLS。Nginx配合SDK做流媒体服务器的原理是: Nginx通过rtmp模块提供rtmp服务, SDK推送一个rtmp流到Nginx, 然后客户端通过访问Nginx来收看实时视频流。 HLS也是差不多的原理,只是最终客户端是通过HTTP协议来访问的,但是SDK推送流仍然是rtmp的。集成rtmp模块的windows版本的Nginx。文章下点赞+私信我获取!1、rtmp端口配置配置文件在/conf/nginx.confRTMP监听 1935 端口,启用live 和hls 两个application所以你的流媒体服务器url可以写成:rtmp://(服务器IP地址):1935/live/xxx 或 rtmp://(服务器IP地址):1935/hls/xxx例如我们上面写的 rtmp://192.168.1.104:1935/live/12345HTTP监听 8080 端口,:8080/stat 查看stream状态:8080/index.html 为一个直播播放与直播发布测试器:8080/vod.html 为一个支持RTMP和HLS点播的测试器2、启动nginx服务双击nginx.exe文件或者在dos窗口下运行nginx.exe,即可启动nginx服务:1)启动任务管理器,可以看到nginx.exe进程 2)打开网页输入http://localhot:8080,出现如下画面: 显示以上界面说明启动成功。第三部分:直播流的播放主播界面: 上面说过了只要支持RTMP流传输协议的播放器都可以收看到我们的直播。下面举两个例子吧: (1)window端播放器VLC (2)android端播放器ijkplayer private void initPlayer() { player = new PlayerManager(this); player.setFullScreenOnly(true); player.setScaleType(PlayerManager.SCALETYPE_FILLPARENT); player.playInFullScreen(true); player.setPlayerStateListener(this); player.play(“rtmp://192.168.1.104:1935/live/12345”); }总结到这里整个基于RTMP推流实现Android视频直播的项目已经完成了,如有你有更好的想法可以在文章下方评论留言或私信我!另外前文中第二部分提到的推流SDK和Android实现的Demo以及第三部分提到的已经集成rtmp模块的windows版本的Nginx下载地址由于发文规则不允许插入外部链接,如有需要的可以再文章下点赞+评论后,下载地址我会私信发给你如回复不及时欢迎加入Android开发技术交流群:150923287! ...

January 14, 2019 · 6 min · jiezi

nginx实现一个域名配置多个laravel项目

背景随着公司的子项目越来越多,会有大大小小十几个工程(仅后端),按照原先的做法,每上线一个项目,那么必须要有一个二级域名映射到对应的工程上,十个工程那么就意味着需要有十个二级域名(还不包含测试环境,次生产环境等),如此多的域名不仅仅是难于管理,更重要的是比较浪费资源,这个问题困扰了我很久,今天终于解决了这个问题,特此记录一下采坑日记,本文不会讲nginx中各个指令的原理,而是用实际的项目配置来练习nginx指令的用法并举一反三。事先准备域名假设域名为:http://www.dev.com实验环境阿里云ECS + centos + Nginx + php-fpm项目11.工程路径: /data/wwwroot/project1/2.访问路径:http://www.dev.com/project1/项目21.工程路径: /data/wwwroot/project2/2.访问路径:http://www.dev.com/project2/项目31.工程路径: /data/wwwroot/project3/2.访问路径:http://www.dev.com/project3/涉及的知识点Nginx的location指令,用法可以参考:https://www.cnblogs.com/coder…Nginx的alias指令,用法可以参考:https://www.jianshu.com/p/4be…实现步骤为了实现以上的访问形式,我们需要用到nginx里面的location指令和alias指令,配置如下location ^~ /${PROJECT}/ { alias {$PATH}; try_files $uri $uri/ @${PROJECT}; location ~ .php$ { fastcgi_pass unix:/dev/shm/php-cgi.sock; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $request_filename; include fastcgi_params; }}location @${PROJECT}{ rewrite /${PROJECT}/(.)$ /${PROJECT}/index.php?/$1 last;}说明: 上面的这个配置中的${PROJECT}和{$PATH}都是属于在实际过程中需要替换的部分,其中${PROJECT}为url需要访问的path部分,如project1,{$PATH}则代表的是项目的真实访问路径,如/data/wwwroot/project1,以http://www.dev.com/project1 访问为例,那么对应的Nginx的配置是这样子的location ^~ /project1/ { alias /data/wwwroot/project1/public; try_files $uri $uri/ @project1; location ~ .php$ { fastcgi_pass unix:/dev/shm/php-cgi.sock; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $request_filename; include fastcgi_params; }}location @project1{ rewrite /project1/(.)$ /project1/index.php?/$1 last;}对于project2和project3的配置只需要按照上面的配置模板依葫芦画瓢就可以了,最后完整nginx配置如下server { listen 80; server_name http://www.dev.com; access_log /data/wwwlogs/nginx/access_log/www.dev.com_nginx.log combined; error_log /data/wwwlogs/nginx/error_log/www.dev.com_errr_log; index index.html index.htm index.php; # project1开始的配置 location ^~ /project1/ { alias /data/wwwroot/project1/public; try_files $uri $uri/ @project1; location ~ .php$ { fastcgi_pass unix:/dev/shm/php-cgi.sock; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $request_filename; include fastcgi_params; } } location @project1{ rewrite /project1/(.)$ /project1/index.php?/$1 last; } # project2开始的配置 location ^~ /project2/ { alias /data/wwwroot/project2/public; try_files $uri $uri/ @project2; location ~ .php$ { fastcgi_pass unix:/dev/shm/php-cgi.sock; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $request_filename; include fastcgi_params; } } location @project2{ rewrite /project2/(.)$ /project2/index.php?/$1 last; } # project2开始的配置 location ^~ /project3/ { alias /data/wwwroot/project3/public; try_files $uri $uri/ @project3; location ~ .php$ { fastcgi_pass unix:/dev/shm/php-cgi.sock; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $request_filename; include fastcgi_params; } } location @project3{ rewrite /project3/(.)$ /project3/index.php?/$1 last; } # 解析所有的.php location ~ .php$ { fastcgi_pass unix:/dev/shm/php-cgi.sock; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; #fastcgi_param SCRIPT_FILENAME $request_filename; include fastcgi_params; } #图片、视频的的链接,此处是做缓存 ,缓存30天,不写入访问日志 location ~ ..(gif|jpg|jpeg|png|bmp|swf|flv|mp4|ico)$ { expires 30d; access_log off; } #js css文件的配置,此处是做缓存 ,缓存7天,不写入访问日志 location ~ .*.(js|css)?$ { expires 7d; access_log off; } location ~ /.ht { deny all; }}参考地址怎么在 localhost 下访问多个 Laravel 项目 ...

January 14, 2019 · 2 min · jiezi

ONE-sys 整合前后端脚手架 koa2 + pm2 + vue-cli3.0 + element

项目地址https://github.com/fanshyiis/…本脚手架主要致力于前端工程师的快速开发、一键部署等快捷开发框架,主要目的是想让前端工程师在一个阿里云服务器上可以快速开发部署自己的项目。本着前端后端融合统一的逻辑进行一些轮子的整合、并加入了自己的一些脚手架工具,第一次做脚手架的开发,如有问题,请在issue上提出,如果有帮助到您的地方,请不吝赐个star技术栈选择前端整合:vue-cli3.0、axios、element等命令行工具整合:commander、chalk、figlet、shelljs等后端整合:node、 koa2、koa-mysql-session、mysql等服务器整合:nginx、pm2、node等基本功能模块实现聚合分离所谓聚合分离,首先是‘聚合’,聚合代码,聚合插件,做到一个项目就可完成前端端代码的编写,打包上线等功能的聚合。其后是‘分离’。前后端分离。虽然代码会在同一个项目工程中但是前后端互不干扰,分别上线,区别于常规的ejs等服务端渲染的模式,做到前端完全分离一键部署基于本地的命令行工具,可以快速打包view端的静态文件并上传到阿里云服务器,也可快速上传server端的文件到服务器文件夹,配合pm2的监控功能进行代码的热更新,无缝更新接口逻辑快速迭代提供基本的使用案例,包括前端的view层的容器案例与组件案例,组件的api设定以及集合了axios的中间件逻辑,方便用户快速搭建自己的项目,代码清晰,易于分析与修改,server端对mysql连接池进行简单的封装,完成连接后及时释放,对table表格与函数进行分层,代码分层为路由层、控制器层、sql操作层基本模块举例1.登录页面登录 -正确反馈 错误反馈 登录成功后session的设定注册 -重名检测 正确反馈 错误反馈主要模块功能模块增删查改基本功能的实现后台koa2服务模块配合koa-mysql-session进行session的设定储存checkLogin中间件的实现cors跨域白名单的设定middlewer 中间件的设定mysql连接池的封装等等。。。服务端nginx 的基本配置与前端端分离的配置pm2 多实例构建配置文件的配置文件 pm2config.json使用流程本地调试安装mysql (过程请百度)// 进入sql命令行$ mysql -u root -p// 创建名为nodesql的数据库$ create database nodesql安装pm2 (过程请百度)拉取项目代码git clone https://github.com/fanshyiis/ONE-syscd ONE-sys// 安装插件cnpm i 或 npm i 或者 yarn add// 安装linksudo npm link// 然后就能使用命令行工具了one start// 或者不愿意使用命令行的同学可以yarn run serve主要代码解析代码逻辑serverbinone -h启动效果启动项目yarn run v1.3.2$ pm2 restart ./server/index.js && vue-cli-service serveUse –update-env to update environment variables[PM2] Applying action restartProcessId on app [./server/index.js](ids: 0,1)[PM2] index ✓[PM2] one-sys ✓┌──────────┬────┬─────────┬─────────┬───────┬────────┬─────────┬────────┬─────┬───────────┬───────────┬──────────┐│ App name │ id │ version │ mode │ pid │ status │ restart │ uptime │ cpu │ mem │ user │ watching │├──────────┼────┼─────────┼─────────┼───────┼────────┼─────────┼────────┼─────┼───────────┼───────────┼──────────┤│ index │ 0 │ 0.1.0 │ fork │ 77439 │ online │ 2640 │ 0s │ 0% │ 15.4 MB │ koala_cpx │ disabled ││ one-sys │ 1 │ 0.1.0 │ cluster │ 77438 │ online │ 15 │ 0s │ 0% │ 20.2 MB │ koala_cpx │ disabled │└──────────┴────┴─────────┴─────────┴───────┴────────┴─────────┴────────┴─────┴───────────┴───────────┴──────────┘ Use pm2 show &lt;id|name&gt; to get more details about an app INFO Starting development server… 98% after emitting CopyPlugin DONE Compiled successfully in 10294ms16:31:55 App running at: - Local: http://localhost:8080/ - Network: http://192.168.7.69:8080/ Note that the development build is not optimized. To create a production build, run yarn build.页面展示线上调试阿里云服务器文件存放目录[root@iZm5e6naugml8q0362d8rfZ ]# cd /home/[root@iZm5e6naugml8q0362d8rfZ home]# lsdist server test[root@iZm5e6naugml8q0362d8rfZ home]#阿里云nginx配置 location ^ /api { proxy_pass http://127.0.0.1:3000; } location ^ /redAlert/ { root /home/dist/; try_files $uri $uri/ /index.html =404; } location ^~ /file/ { alias /home/server/controller/public/; } location / { root /home/dist/; index index.html index.htm; }其他方面如同本地配置有问题可以加群联系最后请star一个吧~~~ ...

January 14, 2019 · 2 min · jiezi

LNMP+HAProxy+Keepalived负载均衡(三)- 配置文件汇总

Nginx的操作命令vim /usr/local/nginx/conf/nginx.conf# 将端口由80修改为10001,修改内容如下:listen 10001 default_server;# 具体配置可参考下面的nginx配置文件# 重启Nginx,并查看其状态;service nginx restart & service nginx statusNginx的配置文件(Web服务器需要修改的配置,仅用参考)user www www;worker_processes auto;error_log /home/wwwlogs/nginx_error.log crit;pid /usr/local/nginx/logs/nginx.pid;#Specifies the value for maximum file descriptors that can be opened by this process.worker_rlimit_nofile 51200;events{ use epoll; worker_connections 51200; multi_accept on;}http{ include mime.types; default_type application/octet-stream; server_names_hash_bucket_size 128; client_header_buffer_size 32k; large_client_header_buffers 4 32k; client_max_body_size 50m; sendfile on; tcp_nopush on; keepalive_timeout 60; tcp_nodelay on; fastcgi_connect_timeout 300; fastcgi_send_timeout 300; fastcgi_read_timeout 300; fastcgi_buffer_size 64k; fastcgi_buffers 4 64k; fastcgi_busy_buffers_size 128k; fastcgi_temp_file_write_size 256k; gzip on; gzip_min_length 1k; gzip_buffers 4 16k; gzip_http_version 1.1; gzip_comp_level 2; gzip_types text/plain application/javascript application/x-javascript text/javascript text/css application/xml application/xml+rss; gzip_vary on; gzip_proxied expired no-cache no-store private auth; gzip_disable “MSIE [1-6].”; #limit_conn_zone $binary_remote_addr zone=perip:10m; ##If enable limit_conn_zone,add “limit_conn perip 10;” to server section. server_tokens off; access_log off; server { # 端口根据自己的情况修改 listen 10001 default_server; server_name _; index index.html index.htm index.php default.html default.htm default.php; # 站点根目录 root /home/wwwroot/publishPath; include rewrite/laravel.conf; #error_page 404 /404.html; # Deny access to PHP files in specific directory #location ~ /(wp-content|uploads|wp-includes|images)/..php$ { deny all; } include enable-php.conf; location ~ ..(gif|jpg|jpeg|png|bmp|swf)$ { expires 30d; } location ~ ..(js|css)?$ { expires 12h; } location ~ /.well-known { allow all; } location ~ /. { deny all; } access_log off; } # 可以加载自己的配置文件,这里我把配置文件中的内容直接替换了原本的server节点配置; # include vhost/.conf;}MySQL的操作命令vim /etc/my.cnfservice mysql restart & service mysql statuslnmp restartMySQL的配置文件(DB服务器需要修改的配置,仅用参考)[client]port = 10002socket = /tmp/mysql.sock[mysqld]port = 10002socket = /tmp/mysql.sock# 数据库文件存放位置datadir = /home/lnmp/mysql/dataskip-external-lockingkey_buffer_size = 128Mmax_allowed_packet = 1Mtable_open_cache = 512sort_buffer_size = 2Mnet_buffer_length = 8Kread_buffer_size = 2Mread_rnd_buffer_size = 512Kmyisam_sort_buffer_size = 32Mthread_cache_size = 64query_cache_size = 64Mtmp_table_size = 64Mperformance_schema_max_table_instances = 4000explicit_defaults_for_timestamp = true#skip-networkingmax_connections = 500max_connect_errors = 100open_files_limit = 65535log-bin=mysql-binbinlog_format=mixedserver-id = 51lower_case_table_names = 1expire_logs_days = 10replicate_wild_do_table=lgd_system.%# relay_log=mysqld-relay-binlog-slave-updates=YESdefault_storage_engine = InnoDBinnodb_file_per_table = 1innodb_data_home_dir = /home/lnmp/mysql/datainnodb_data_file_path = ibdata1:10M:autoextendinnodb_log_group_home_dir = /home/lnmp/mysql/datainnodb_buffer_pool_size = 512Minnodb_log_file_size = 128Minnodb_log_buffer_size = 8Minnodb_flush_log_at_trx_commit = 1innodb_lock_wait_timeout = 50[mysqldump]# 数据库备份账户,自行创建并分配相应的权限user=bakuserpassword=ZXdfty^&quickmax_allowed_packet = 16M[mysql]no-auto-rehash[myisamchk]key_buffer_size = 128Msort_buffer_size = 2Mread_buffer = 2Mwrite_buffer = 2M[mysqlhotcopy]interactive-timeoutHAProxy的操作命令# 负载状态监测:# Web服务器HAProxy - http://192.168.6.111:8080/web# DB服务器HAProxy - http://192.168.6.211:8080/db# 如果需要通过外网访问,需要把8080端口映射到外网端口即可。# 常用命令:vim /etc/haproxy/haproxy.cfgservice haproxy restart & service haproxy statusHAProxy的配置文件(Web服务器)#———————————————————————# Global settings#———————————————————————global # 全局的日志配置,使用log关键字,指定使用127.0.0.1上的syslog服务中的local0日志设备,记录日志等级为info的日志 log 127.0.0.1 local3 # 软件工作目录 chroot /var/lib/haproxy # haproxy的pid存放路径,启动进程的用户必须有权限访问此文件 pidfile /usr/local/haproxy/haproxy.pid # 最大连接数,默认4000 maxconn 30000 # 所属用户 user haproxy # 所属组 group haproxy # 以守护进程方式运行haproxy daemon # turn on stats unix socket # stats socket /var/lib/haproxy/stats # socket路径#———————————————————————# common defaults that all the ’listen’ and ‘backend’ sections will# use if not designated in their block#———————————————————————defaults mode http # 默认的模式mode { tcp|http|health },tcp是4层,http是7层,health只会返回OK log global # 采用全局定义的日志 option httplog # 启用日志记录HTTP请求,默认haproxy日志记录是不记录HTTP请求日志 option dontlognull # 不记录健康检查的日志信息 option http-server-close # 每次请求完毕后主动关闭http通道 # 如果后端服务器需要获得客户端真实ip需要配置的参数,可以从Http Header中获得客户端ip option forwardfor except 127.0.0.0/8 option redispatch # serverId对应的服务器挂掉后,强制定向到其他健康的服务器 retries 3 # 3次连接失败就认为服务不可用,也可以通过后面设置 timeout http-request 10s # http请求超时时间 timeout queue 1m # 一个请求在队列里的超时时间 timeout connect 10s # 连接超时 timeout client 1m # 客户端连接超时 timeout server 1m # 服务器连接超时 timeout http-keep-alive 10s # 设置http-keep-alive的超时时间 timeout check 10s # 检测超时 maxconn 3000 # 最大连接数#———————————————————————# main frontend which proxys to the backends#———————————————————————# 前端配置frontend main *:80 acl url_static path_beg -i /static /images /javascript /stylesheets acl url_static path_end -i .jpg .gif .png .css .js use_backend static if url_static default_backend servers#———————————————————————# static backend for serving up images, stylesheets and such#———————————————————————# 后台静态文件服务配置backend static balance roundrobin server static1 192.168.6.100:10001 check inter 2000 fall 3 weight 50 server static2 192.168.6.110:10001 check inter 2000 fall 3 weight 50#———————————————————————# round robin balancing between the various backends#———————————————————————# 后台服务配置backend servers balance roundrobin # 添加cookie配置,将某客户端引导到之前为其服务过的后端服务器上,即和后端某服务器保持联系,防止登录验证失效 cookie app_cook insert nocache server app1 192.168.6.100:10001 check inter 2000 fall 3 weight 50 cookie server1 server app2 192.168.6.110:10001 check inter 2000 fall 3 weight 50 cookie server2# HAProxy状态监控服务配置listen stats # 绑定端口 bind *:8080 mode http # stats enable # 访问地址:192.168.6.100:8080/web 和 192.168.6.110:8080/web stats uri /web stats realm Global\ statistics # 管理员账户 stats auth hapadmin:1qazse$#2HAProxy的配置文件(DB服务器)#———————————————————————# Global settings#———————————————————————global pidfile /var/run/haproxy.pid maxconn 30000 user haproxy group haproxy daemon nbproc 1#———————————————————————# common defaults that all the ’listen’ and ‘backend’ sections will# use if not designated in their block#———————————————————————defaults mode tcp option redispatch retries 3 timeout queue 1m timeout connect 10s timeout client 1m timeout server 1m timeout check 10s maxconn 4096 option abortonclosefrontend main bind :3306 default_backend serversbackend servers server mysql1 192.168.6.200:10002 check inter 3000 fall 3 weight 50 server mysql2 192.168.6.210:10002 check inter 3000 fall 3 weight 50# 监控访问地址:192.168.6.210:8080/db 和 192.168.6.200:8080/dblisten stats mode http bind 0.0.0.0:8080 stats enable stats uri /db stats realm Global\ statistics stats auth dbadmin:1qazse$#2Keeplived的操作命令# 查看已安装的Keepalived的版本:keepalived -v# 查看配置:cat /etc/keepalived/keepalived.conf# 编辑配置文件:vim /etc/keepalived/keepalived.conf# 测试高可用的远程访问:mysql -h 远程数据库ip地址 -P 端口 -u 用户名 -pmysql -h 192.168.6.200 -P 3306 -u dbuser -p# 开通服务器间的 vrrp 协议通信,用于Keepalived通信:firewall-cmd –direct –permanent –add-rule ipv4 filter INPUT 0 –in-interface 网卡名称 –destination 224.0.0.18 –protocol vrrp -j ACCEPT;firewall-cmd –reload;# 服务器的网卡名称请根据自己的情况修改,# INPUT代表接收224.0.0.18的报文。# 在VIP服务器上测试VIP漂移:ip addr | grep 网卡名称# 停止VIP所在服务器的keepalived服务,并查看VIP是否移除,并查看备用服务器是否获取到VIP:service keepalived stop && service keepalived statusip addr | grep 网卡名称# 在之前停止keepalived服务的服务器上开启keepalived服务,查看VIP是否已取回:service keepalived start && service keepalived statusip addr | grep 网卡名称Keeplived的配置(Web服务器)Web主服务器的配置:# Master的配置内容:! Configuration File for keepalivedglobal_defs { notification_email { example@domain.com # 收邮件人,可以定义多个 } notification_email_from HaproxyMaster@web.haproxy # 发件人,可伪装 smtp_server 127.0.0.1 # 发送邮件的服务器地址 smtp_connect_timeout 30 # 连接超时时间 no_email_faults router_id WebMaster vrrp_skip_check_adv_addr vrrp_strict vrrp_garp_interval 0 vrrp_gna_interval 0}vrrp_script chk_haproxy { # HAProxy服务监测脚本 script ‘/etc/keepalived/check_haproxy.sh’ interval 2 weight 2}vrrp_instance VI_1 { # 每一个vrrp_instance就是定义一个虚拟路由器 state MASTER # 由初始状态状态转换为master状态 interface 网卡名称 # 网卡名称,如eth0,根据自己的情况修改 virtual_router_id 100 # 虚拟路由的id号,一般不能大于255的 priority 100 # 优先级,数字越大,优先级越高,主比次大 advert_int 1 # 初始化通告 authentication { # 认证机制 auth_type PASS auth_pass 666 # 密码,自行更改,主备一致即可 } track_script { chk_haproxy } virtual_ipaddress { # Web服务的虚拟ip地址:vip,前面提到的备用的虚拟IP。 #<IPADDR>/<MASK> brd <IPADDR> dev <STRING> scope <SCOPT> label <LABEL> #192.168.200.18/24 dev eth2 label eth2:1 192.168.6.111 } notify_master ‘/etc/keepalived/clean_arp.sh 192.168.6.111’}Web备服务器的配置:# Backup的配置内容:! Configuration File for keepalivedglobal_defs { notification_email { example@domain.com # 收邮件人,可以定义多个 } notification_email_from HaproxyBackup@web.haproxy # 发件人,可伪装 smtp_server 127.0.0.1 # 发送邮件的服务器地址 smtp_connect_timeout 30 # 连接超时时间 no_email_faults router_id WebBackup vrrp_skip_check_adv_addr vrrp_strict vrrp_garp_interval 0 vrrp_gna_interval 0}vrrp_script chk_haproxy { # HAProxy服务监测脚本 script ‘/etc/keepalived/check_haproxy.sh’ interval 2 weight 2}vrrp_instance VI_1 { # 每一个vrrp_instance就是定义一个虚拟路由器 state BACKUP # 由初始状态状态转换为backup状态 interface 网卡名称 # 网卡名称,如eth0,根据自己的情况修改 virtual_router_id 100 # 虚拟路由的id号,一般不能大于255的 priority 90 # 优先级,数字越大,优先级越高,主比次大 advert_int 1 # 初始化通告 authentication { # 认证机制 auth_type PASS auth_pass 666 # 密码,自行更改,主备一致即可 } track_script { chk_haproxy } virtual_ipaddress { # Web服务的虚拟ip地址:vip,前面提到的备用的虚拟IP。 #<IPADDR>/<MASK> brd <IPADDR> dev <STRING> scope <SCOPT> label <LABEL> #192.168.200.18/24 dev eth2 label eth2:1 192.168.6.111 } notify_master ‘/etc/keepalived/clean_arp.sh 192.168.6.111’}Keeplived的配置(DB服务器)DB主服务器的配置:# Master的配置内容:! Configuration File for keepalivedglobal_defs { notification_email { example@domain.com # 收邮件人,可以定义多个 } notification_email_from HaproxyMaster@db.haproxy # 发件人,可伪装 smtp_server 127.0.0.1 # 发送邮件的服务器地址 smtp_connect_timeout 30 # 连接超时时间 no_email_faults router_id DBMaster vrrp_skip_check_adv_addr vrrp_strict vrrp_garp_interval 0 vrrp_gna_interval 0}vrrp_script chk_haproxy { # HAProxy服务监测脚本 script ‘/etc/keepalived/check_haproxy.sh’ interval 2 weight 2}vrrp_instance VI_1 { # 每一个vrrp_instance就是定义一个虚拟路由器 state MASTER # 由初始状态状态转换为master状态 interface 网卡名称 # 网卡名称,如eth0,根据自己的情况修改 virtual_router_id 99 # 虚拟路由的id号,一般不能大于255的 priority 100 # 优先级,数字越大,优先级越高,主比次大 advert_int 1 # 初始化通告 authentication { # 认证机制 auth_type PASS auth_pass 666 # 密码,自行更改,主备一致即可 } track_script { chk_haproxy } virtual_ipaddress { # DB服务的虚拟ip地址:vip,前面提到的备用的虚拟IP。 #<IPADDR>/<MASK> brd <IPADDR> dev <STRING> scope <SCOPT> label <LABEL> #192.168.200.18/24 dev eth2 label eth2:1 192.168.6.211 } notify_master ‘/etc/keepalived/clean_arp.sh 192.168.6.211’}DB备服务器的配置:# Backup的配置内容:! Configuration File for keepalivedglobal_defs { notification_email { example@domain.com # 收邮件人,可以定义多个 } notification_email_from HaproxyBackup@db.haproxy # 发件人,可伪装 smtp_server 127.0.0.1 # 发送邮件的服务器地址 smtp_connect_timeout 30 # 连接超时时间 no_email_faults router_id DBBackup vrrp_skip_check_adv_addr vrrp_strict vrrp_garp_interval 0 vrrp_gna_interval 0}vrrp_script chk_haproxy { # HAProxy服务监测脚本 script ‘/etc/keepalived/check_haproxy.sh’ interval 2 weight 2}vrrp_instance VI_1 { # 每一个vrrp_instance就是定义一个虚拟路由器 state BACKUP # 由初始状态状态转换为master状态 interface 网卡名称 # 网卡名称,如eth0,根据自己的情况修改 virtual_router_id 99 # 虚拟路由的id号,一般不能大于255的 priority 90 # 优先级,数字越大,优先级越高,主比次大 advert_int 1 # 初始化通告 authentication { # 认证机制 auth_type PASS auth_pass 666 # 密码,自行更改,主备一致即可 } track_script { chk_haproxy } virtual_ipaddress { # DB服务的虚拟ip地址:vip,前面提到的备用的虚拟IP。 #<IPADDR>/<MASK> brd <IPADDR> dev <STRING> scope <SCOPT> label <LABEL> #192.168.200.18/24 dev eth2 label eth2:1 192.168.6.211 } notify_master ‘/etc/keepalived/clean_arp.sh 192.168.6.211’}创建Keepalived调用的脚本操作命令mkdir /etc/keepalived/echo ’’ > /etc/keepalived/check_haproxy.shecho ’’ > /etc/keepalived/clean_arp.shchmod +x /etc/keepalived/.sh# 然后编辑两个脚本的内容,如下/etc/keepalived/check_haproxy.sh#!/bin/bash# 判断haproxy是否已经启动if [ $(ps -C haproxy –no-header | wc -l) -eq 0 ]; then # 如果没有启动,则启动haproxy程序 haproxy -f /etc/haproxy/haproxy.cfgfi# 睡眠两秒钟,等待haproxy完全启动sleep 2# 判断haproxy是否已经启动if [ $(ps -C haproxy –no-header | wc -l) -eq 0 ]; then # 如果haproxy没有启动起来,则将keepalived停掉,则VIP自动漂移到另外一台haproxy机器,实现了对haproxy的高可用 service keepalived stop/etc/keepalived/clean_arp.sh#!/bin/shVIP=$1GATEWAY=192.168.6.255 # 本机的网卡网关地址/sbin/arping -I ens160 -c 5 -s $VIP $GATEWAY &>/dev/null发布文件的配置# 站点根目录赋权chmod -R 777 /home/wwwroot/publishPath# PHP环境配置vim /home/wwwroot/publishPath/.env# 编辑配置内容:APP_DEBUG=false# Web的内网VIP,如需外网访问,则需要将192.168.6.111映射到外网,然后将该处的IP改成外网IPAPP_URL=http://192.168.6.111DB_CONNECTION=mysql# DB的内网VIPDB_HOST=192.168.6.211# DB的内网端口DB_PORT=3306# 数据库名称DB_DATABASE=dbName# 数据库用户名DB_USERNAME=dbuser# 数据库密码DB_PASSWORD=dbpwd# 其他配置选项使用默认设置,这里省略。# 配置保存退出后重启php服务:service php-fpm restart关于IP的说明 以上说到的IP都是内网IP,所有的配置都使用内网IP即可。如需外网访问,只需要把两个虚拟IP和端口映射到外网即可(注意修改php配置的APP_URL)。 ...

January 14, 2019 · 6 min · jiezi

彻底弄懂跨域问题

跨域,老生常谈的问题简述作为一只前端菜鸟,跨域方面只懂得JSONP和CORS,并未曾深入了解。但随着春招越来越近,就算是菜鸟也要猛振翅膀。近几日仔细研究了跨域问题,写下这篇文章,希望对开发者们有所帮助。在读本文前,希望您对以下知识略有了解。浏览器同源策略nodejsiframedocker, nginx我们为何要研究跨域问题因为浏览器的同源策略规定某域下的客户端在没明确授权的情况下,不能读写另一个域的资源。而在实际开发中,前后端常常是相互分离的,并且前后端的项目部署也常常不在一个服务器内或者在一个服务器的不同端口下。前端想要获取后端的数据,就必须发起请求,如果不错一些处理,就会受到浏览器同源策略的约束。后端可以收到请求并返回数据,但是前端无法收到数据。多种跨域方法跨域可以大概分为两种目的前后端分离时,前端为了获取后端数据而跨域为不同域下的前端页面通信而跨域为前后端分离而跨域Cross Origin Resource Share (CORS)CORS是一个跨域资源共享方案,为了解决跨域问题,通过增加一系列请求头和响应头,规范安全地进行跨站数据传输请求头主要包括请求头解释OriginOrigin头在跨域请求或预先请求中,标明发起跨域请求的源域名。Access-Control-Request-MethodAccess-Control-Request-Method头用于表明跨域请求使用的实际HTTP方法Access-Control-Request-HeadersAccess-Control-Request-Headers用于在预先请求时,告知服务器要发起的跨域请求中会携带的请求头信息响应头主要包括响应头解释Access-Control-Allow-OriginAccess-Control-Allow-Origin头中携带了服务器端验证后的允许的跨域请求域名,可以是一个具体的域名或是一个*(表示任意域名)。Access-Control-Expose-HeadersAccess-Control-Expose-Headers头用于允许返回给跨域请求的响应头列表,在列表中的响应头的内容,才可以被浏览器访问。Access-Control-Max-AgeAccess-Control-Max-Age用于告知浏览器可以将预先检查请求返回结果缓存的时间,在缓存有效期内,浏览器会使用缓存的预先检查结果判断是否发送跨域请求。Access-Control-Allow-MethodsAccess-Control-Allow-Methods用于告知浏览器可以在实际发送跨域请求时,可以支持的请求方法,可以是一个具体的方法列表或是一个*(表示任意方法)。如何使用客户端只需按规范设置请求头。服务端按规范识别并返回对应响应头,或者安装相应插件,修改相应框架配置文件等。具体视服务端所用的语言和框架而定SpringBoot 设置CORS例子一个spring boot项目中关于CORS配置的一段代码HttpServletResponse httpServletResponse = (HttpServletResponse) response; String temp = request.getHeader(“Origin”); httpServletResponse.setHeader(“Access-Control-Allow-Origin”, temp); // 允许的访问方法 httpServletResponse.setHeader(“Access-Control-Allow-Methods”, “POST, GET, PUT, OPTIONS, DELETE, PATCH”);// Access-Control-Max-Age 用于 CORS 相关配置的缓存 httpServletResponse.setHeader(“Access-Control-Max-Age”, “3600”); httpServletResponse.setHeader(“Access-Control-Allow-Headers”, “Origin, X-Requested-With, Content-Type, Accept,token”); httpServletResponse.setHeader(“Access-Control-Allow-Credentials”, “true”);JSONP 跨域jsonp的原理就是借助HTML中的<script>标签可以跨域引入资源。所以动态创建一个<srcipt>标签,src为目的接口 + get数据包 + 处理数据的函数名。后台收到GET请求后解析并返回函数名(数据)给前端,前端<script>标签动态执行处理函数观察下面代码前端代码<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <title>Title</title></head><body><script> var script = document.createElement(‘script’); script.type = ’text/javascript’; // 传参并指定回调执行函数为getData script.src = ‘http://localhost:8080/users?username=xbc&callback=handleData’; document.body.appendChild(script); // 回调执行函数 function handleData(res) { data = JSON.stringify(res) console.log(data); }</script></body></html>后端代码(nodejs)var querystring = require(‘querystring’);var http = require(‘http’);var server = http.createServer();server.on(‘request’, function(req, res) { var params = querystring.parse(req.url.split(’?’)[1]); var fn = params.callback; // jsonp返回设置 res.writeHead(200, { ‘Content-Type’: ’text/javascript’ }); var data = { user: ‘xbc’, password: ‘123456’ } res.write(fn + ‘(’ + JSON.stringify(data) + ‘)’); res.end();});server.listen(‘8080’);console.log(‘Server is running at port 8080…’);在该例子中,前台收到的res是这样的前端页面是这样的注意JSONP既是利用了<srcipt>,那么就只能支持GET请求。其他请求无法实现nginx 反向代理实现跨域思路既然浏览器有同源策略限制,那我们把前端项目和前端要请求的api接口地址放在同源下不就可以了?再结合web服务器提供的反向代理,便可以在前端和后端都不做配置的情况下解决跨域问题。以nginx为例后端真实后台地址:http://xxx.xxx.xxx.xxx:8085 后台地址使用tomcat部署的spring boot项目 名为gsms_testnginx服务器地址: http://xxx.xxx.xxx.xxx:8082tomcat和nginx都是用docker架设的,做了端口转发nginx /etc/nginx/conf.d/default.conf配置代码如下server { listen 80; server_name localhost; #charset koi8-r; #access_log /var/log/nginx/host.access.log main; location / { root /usr/share/nginx/html/dist; # 前端项目路径 index index.html index.htm; autoindex on; autoindex_exact_size on; autoindex_localtime on; } location /gsms_test/ { proxy_pass 后端真实地址; } #error_page 404 /404.html; # redirect server error pages to the static page /50x.html # error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } # proxy the PHP scripts to Apache listening on 127.0.0.1:80 # #location ~ .php$ { # proxy_pass http://127.0.0.1; #} # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 # #location ~ .php$ { # root html; # fastcgi_pass 127.0.0.1:9000; # fastcgi_index index.php; # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; # include fastcgi_params; #} # deny access to .htaccess files, if Apache’s document root # concurs with nginx’s one # #location ~ /.ht { # deny all; #}}不同域下页面通信而跨域window.name + iframe 跨域window.name是浏览器中一个窗口所共享的数据,在不同的页面(甚至不同域名)加载后依旧存在(如果没修改则值不会变化),并且可以支持非常长的 name 值(2MB)。比如 a域的某页面想获取b域某页面的数据,可以在b域中修改window.name值,a域切换到b域再切回来即可得到b域的window.name值。可是我们在开发中肯定不想页面切来切去,所以就要结合iframe来实现。示例 (以thinkjs实现)a 域代码如下<!DOCTYPE html><html><head><meta charset=“UTF-8”><title>A 域</title></head><body><h1>server A</h1><script type=“text/javascript”> function getData() { var iframe = document.getElementById(‘proxy’); iframe.onload = function () { var name = iframe.contentWindow.name; // 获取iframe窗口里的window.name值 console.log(name) } // 由于iframe信息传递也受同源策略限制,所以在window.name被B域修改后,将iframe转回A域下。以便获取iframe的window.name值 iframe.src = ‘http://127.0.0.1:8360/sub.html’ }</script><iframe id=“proxy” src=“http://127.0.0.1:8361/index.html” style=“width: 100%” onload=“getData()"> </iframe></body></html>b 域代码<!DOCTYPE html><html><head><meta charset=“UTF-8”><title>New ThinkJS Application</title></head><body> <h1>server 2</h1><script type=“text/javascript”> window.name = ‘user: xbc’;</script></body></html>注意由于受同源策略限制,父页面获取跨域的iframe页面的信息不全,所以要在iframe的window.name被B域修改后,转为A域下的任一页面(该一面不得修改window.name),在进行获取。代理页面 + iframe 实现跨域访问由于iframe与父页面相互访问也受同源策略限制,所以要借助一代理页面实现跨域。个人认为有些麻烦,若有兴趣请看前端如何用代理页面解决iframe跨域访问的问题?总结以上几种皆是本人用过或测试过的跨域方法,还有postMessage,WebSocket等跨域方法由于从未接触不做说明。在项目中具体使用那些方法还需具体考录各种问题情况方法只有GET请求JSONP对兼容性及浏览器版本无要求CROS对兼容性及浏览器版本有要求iframe 或 服务器反向代理本文参考经验 跨域方案CORS——跨域请求那些事儿前端如何用代理页面解决iframe跨域访问的问题?前端常见的跨域解决方案(全)CORS与服务器反向代理的优劣对比图解正向代理、反向代理、透明代理谢谢本文如有错误,欢迎留言私信讨论指出 ...

January 12, 2019 · 2 min · jiezi

LNMP+HAProxy+Keepalived负载均衡 - 基础服务准备

日志服务修改日志服务配置并重启日志服务;vim /etc/rsyslog.conf编辑系统日志配置,指定包含的配置文件路径和规则:$IncludeConfig /etc/rsyslog.d/.conf为haproxy创建一个独立的配置文件;vim /etc/rsyslog.d/haproxy.conf编辑配置文件的内容如下:$ModLoad imudp # 取消注释$UDPServerRun 514 # 取消注释# 与“/etc/haproxy/haproxy.cfg”中的配置“log 127.0.0.1 local3”对应local3. /var/log/haproxy.log# 如果不加 “&~”,则除了在/var/log/haproxy.log中写入日志外,也会写入message文件&~配置“rsyslog”的主配置文件,开启远程日志;vim /etc/sysconfig/rsyslog修改配置内容如下:SYSLOGD_OPTIONS="-c 2 -r -m 0"# -c 2 使用兼容模式,默认是 -c 5# -r 开启远程日志# -m 0 标记时间戳,单位是分钟,为0表示禁用该功能重启HAProxy和日志服务并查看各自服务状态:service haproxy restart & service haproxy statusservice rsyslog restart & service rsyslog status# 查看PHP的错误日志配置cat /usr/local/php/etc/php.ini | grep error_log防火墙服务开通端口(根据自身需求配置):firewall-cmd –zone=public –add-port=3306/tcp –permanentfirewall-cmd –zone=public –add-port=873/tcp –permanentfirewall-cmd –zone=public –add-port=10002/tcp –permanentfirewall-cmd –zone=public –add-port=10001/tcp –permanentfirewall-cmd –zone=public –add-port=80/tcp –permanentfirewall-cmd –zone=public –add-port=8080/tcp –permanent重启/重新加载防火墙服务并查看其状态:systemctl restart firewalld.serviceservice firewalld restart && service firewalld statusfirewall-cmd –reload测试端口:telnet ip port第三方防火墙 这里推荐semanage,优点自行百度,安装配置:# 安装端口管理工具semanage;yum -y install policycoreutils-python# 查看已开通端口;semanage port -l|grep http# 开通端口;semanage port -a -t http_port_t -p tcp port_number # 开放端口port_number,要开通的端口号semanage port -d -t http_port_t -p tcp port_number # 关闭端口port_number,http_port_t为端口组名其他命令# 查看服务的pid:ps -ef | grep ServiceName# 停止服务:kill -9 service_pid# 查看端口占用情况:lsof -i tcp:80# 列出所有端口:netstat -ntlp# 分区及挂载操作# 查看当前空间df -h# 查看可用磁盘fdisk -lfdisk /dev/sdb# 创建分区,多数操作可以默认Command (m for help): m# 根据提示进行操作:# 分区后格式化mkfs -t ext4 /dev/sdb1mkfs -t ext4 /dev/sdb2# 挂载到已有目录mount -w /dev/sdb1 /mnt/lnmpmount -w /dev/sdb2 /mnt/backupmount -o remount -w /dev/sdb2 /mnt/backup ...

January 12, 2019 · 1 min · jiezi

运维记录1——解决在Nginx下部署CRA项目,二级目录不能访问的问题

如果从头开始搭建React项目,create-react-app通常是开发者的首选。毕竟不是谁都有精力去了解WebPack的复杂配置,而CRA将配置隐藏开箱即用的特性必然会受到普遍欢迎。根目录访问到了部署阶段,我通常使用nginx作为web容器,将项目部署到一个根目录下访问。如# nginx配置server { listen 80; server_name my.website.com; … location / { alias /data/www/react-project/dist; index index.html }}那么只要我们将项目文件放到对应的目录下,重启nginx即可开始访问web页面。二级目录访问有时我们有多个web项目,多个项目不可能同时挂在根目录下,所以我们会划分二级目录来分别访问各个web项目。如http://my.website.com/project1 => 访问react-project1项目http://my.website.com/project2 => 访问react-project2项目问题1:CSS等资源加载失败此时,如果简单将nginx配置的location改为/project1,则会出现网页无法访问的错误。# nginx配置server { listen 80; server_name my.website.com; … # location / { location /project1 { alias /data/www/react-project/dist; index index.html }}现象从dev工具可以看出,html文件有取得,但css、js等资源引用失败。css和js的文件路径都是http://my.website.com/static/…(或css)。分析CRA(create-react-app)的项目配置默认是跑在根目录下的。如果查看dist目录下的html会发现,所有的css或js文件的引用路径都是/开头的绝对路径。解决将打包路径从绝对路径改为相对路径:# package.json{ … “homepage”: “.”, // 添加homepage属性,将路径改为当前目录 …}重新编译后看到,所有的资源文件路径都改过来了。问题2:加载成功,网页空白重新上传到服务器,更新dist目录下的文件,重启nginx后访问网页。现象结果发现,网页仍然是空白一片。查看html的渲染结果,发现似乎js并没有执行。分析在react-router-dom的例子中,通常使用的是BrowserRouter。这种类型的Router在向服务器发送请求时,如果相对于二级目录的路由去指向对应的页面路由,就会找不到资源,因此也就不会渲染。解决BrowserRouter有一个属性叫做basename,就是用于解决此类问题。…import { Route, BrowserRouter as Router, Switch, Redirect } from ‘react-router-dom’;…… <Router basename=’/project1’> <Switch> <Redirect exact key=‘index’ path=’/’ to=’/home’ /> { routes } </Switch> </Router>…问题3:访问成功,刷新后404修改以上配置并编译部署,重启nginx后可正常访问网页。但刷新后,网页变为一片空白。现象网页显示,在请求页面路由如http://my.website.com/project… 时,该路由的请求状态为404。分析还是因为BrowserRouter的问题,之前能正常访问时因为路由中设置了Redirect,所以能访问到根目录并自动跳转到/home。但直接访问则会访问失败。解决在nginx配置中加入try_files命令 location /project1 { alias /data/www/react-project/dist; # index index.html try_files $uri /project1/index.html }这样,在请求$uri时如果找不到对应的资源,会fallback回去加载index.html。问题解决。 ...

January 11, 2019 · 1 min · jiezi

Nginx配置HTTP2.0

Http2.0已经发布很久了,其优点前篇文章已经介绍过了HTTP2-0原理解析,今天我们来配置Nginx使其支持Http2.0安装前必读:Nginx1.10.0以上版本才支持Http2.0,如果使用的是Tengine,版本需要大于2.2.1Http2.0只支持Https协议的网站,且openssl版本需要高于1.0.2一、查看当前Nginx安装了哪些模块(如果事先没有安装Nginx,请忽略此步骤)> /usr/sbin/nginx -Vnginx version: nginx/1.12.2built by gcc 4.8.5 20150623 (Red Hat 4.8.5-16) (GCC)built with OpenSSL 1.0.2k-fips 26 Jan 2017TLS SNI support enabledconfigure arguments: –prefix=/usr/share/nginx –sbin-path=/usr/sbin/nginx –modules-path=/usr/lib64/nginx/modules –conf-path=/etc/nginx/nginx.conf –error-log-path=/var/log/nginx/error.log –http-log-path=/var/log/nginx/access.log –http-client-body-temp-path=/var/lib/nginx/tmp/client_body –http-proxy-temp-path=/var/lib/nginx/tmp/proxy –http-fastcgi-temp-path=/var/lib/nginx/tmp/fastcgi –http-uwsgi-temp-path=/var/lib/nginx/tmp/uwsgi –http-scgi-temp-path=/var/lib/nginx/tmp/scgi –pid-path=/run/nginx.pid –lock-path=/run/lock/subsys/nginx –user=nginx –group=nginx –with-file-aio –with-ipv6 –with-http_ssl_module –with-http_v2_module –with-http_realip_module –with-http_addition_module –with-http_xslt_module=dynamic –with-http_image_filter_module=dynamic –with-http_geoip_module=dynamic –with-http_sub_module –with-http_dav_module –with-http_flv_module –with-http_mp4_module –with-http_gunzip_module –with-http_gzip_static_module –with-http_random_index_module –with-http_secure_link_module –with-http_degradation_module –with-http_slice_module –with-http_stub_status_module –with-http_perl_module=dynamic –with-mail=dynamic –with-mail_ssl_module –with-pcre –with-pcre-jit –with-stream=dynamic –with-stream_ssl_module –with-google_perftools_module –with-debug –with-cc-opt=’-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong –param=ssp-buffer-size=4 -grecord-gcc-switches -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -m64 -mtune=generic’ –with-ld-opt=’-Wl,-z,relro -specs=/usr/lib/rpm/redhat/redhat-hardened-ld -Wl,-E’可以看到http2.0模块–with-http_v2_module已经存在了,方便演示就重新执行一遍二、安装,升级这里以最新版的Tengine为例> wget http://tengine.taobao.org/download/tengine-2.2.3.tar.gz> cd tengine-2.2.3> ./configure –prefix=/usr/share/nginx –with-http_stub_status_module –with-http_ssl_module –with-http_v2_module –with-http_gzip_static_module –with-ipv6 –with-http_sub_module# 执行make编译> make# 如果是首次安装Nginx,执行make install;如果是升级,执行make upgrade> make upgrade# 完成后查看已经安装的模块,可以发现已经有http2(–with-http_v2_module)模块了> nginx -V三、配置NginxNginx配置http2很简单,只需要在listen的端口后新增http2标识即可,如下server { listen 443 ssl http2; server_name hostname.com; ssl_certificate cert/214547145790616.pem; ssl_certificate_key cert/214547145790616.key; location / { proxy_pass http://localhost:12345; }}四、检查http2是否生效打开chrome控制台,在Name一栏右键把Protocol勾选上欢迎订阅「K叔区块链」 - 专注于区块链技术学习 博客地址:http://www.jouypub.com简书主页:https://www.jianshu.com/u/756c9c8ae984segmentfault主页:https://segmentfault.com/blog/jouypub腾讯云主页:https://cloud.tencent.com/developer/column/72548 ...

January 10, 2019 · 1 min · jiezi

LNMP+HAProxy+Keepalived负载均衡 - LNMP基础环境准备

环境版本说明:服务器系统:CentOS 7.5:cat /etc/redhat-releaseCentOS Linux release 7.5.1804 (Core) # 输出结果服务器IP地址: 服务器A:192.168.6.100 服务器B:192.168.6.200LNMP版本: lnmp1.5 下载地址:http://soft.vpser.net/lnmp/ln…准备安装环境(两台服务器都需要执行):# 关闭selinux(如果是centos系统,默认会开启selinux,会引发很多权限问题)vim /etc/selinux/config# 把SELINUX=enforcing改为SELINUX=disabled# 保存退出,并执行下面的命令使配置立即生效:setenforce 0# 升级所有包,改变软件设置和系统设置,系统版本内核都升级# yum -y update# 升级所有包,不改变软件设置和系统设置,系统版本升级,内核不改变yum -y upgrade# 安装后面用到的软件yum -y install haproxy keepalived vim crontabs mlocate && updatedb# 创建文件夹,并将lnmp安装包下载到当前新创建的文件夹mkdir -p /home/soft && cd /home/soft && wget http://soft.vpser.net/lnmp/lnmp1.5-full.tar.gz# 解压安装包tar -xvf lnmp1.5-full.tar.gz安装lnmp:cd /home/soft/lnmp1.5-full./install.sh根据自己的需要选择MySQL、PHP等软件的版本,按提示操作,然后等待安装完成。我这里都选择最新版本。记好设置的相关密码,后面会用到。其他命令集合(仅用参考,无需执行):# 添加用户组和用户,并为其分配相关文件夹的最高权限:groupadd -r GroupName useradd -g UserName -M -s /sbin/nologin GroupNamechown -R GroupName:UserName /usr/local/haproxy# 工具版本查看mysql -uroot -pPwdStr # 登录后就可以看到mysql的版本nginx -v # nginx versionhaproxy -v # HA-Proxy versionkeepalived -v # keepalived 版本# 编辑配置文件集合:vim /etc/keepalived/keepalived.confvim /etc/rsyslog.conf # 编辑系统日志配置vim /etc/rsyslog.d/haproxy.conf # HAProxy的日志vim /etc/sysconfig/rsyslog # rsyslog的主配置vim /usr/local/nginx/conf/nginx.conf # Nginx的配置vim /etc/haproxy/haproxy.cfg # HAProxy的配置vim /etc/my.cnf # MySQL的配置# 将相关服务设置为开机启动:chkconfig nginx on # Web服务chkconfig mysql on # 数据库服务chkconfig haproxy on # 反向代理服务chkconfig keepalived on # 服务状态监测chkconfig crond on # 计划任务服务chkconfig rsyslog on # 日志服务# 重启各服务集合:service haproxy restart & service haproxy statusservice rsyslog restart & service rsyslog statusservice nginx restart & service nginx statusservice mysql restart & service mysql statusservice keepalived restart & service keepalived statusservice crond restart & service crond statuslnmp restart离线安装 如果要安装的服务器无法连接外网,安装就要麻烦很多,无法使用lnmp的一键安装包了。只能通过PC下载,然后远程上传到服务器,然后再编译安装。这里就不列举所有软件的安装。下载MySQL 点击官方下载 mysql-8.0.13-1.el7.x86_64.rpm-bundle.tar;安装MySQL# 卸载系统自带数据库:rpm -qa | grep MySQL-rpm -ev xxxrpm -e –nodeps mysqlyum -y remove mari*# 将下载的文件通过Xftp上传到服务器# 解压文件到当前目录:tar -xvf mysql-8.0.13-1.el7.x86_64.rpm-bundle.tar# 安装 MySQL:rpm -ivh MySQL_# 创建用户组和用户:groupadd -r mysqluseradd -g mysql mysql# 为MySQL的数据库文件夹授权:chown -R mysql:mysql /home/lnmp/mysql/data/# 相应的依赖 # 1. libaio # 2. net-tools # 3. perl# 安装perl./Configure -des -Dprefix=/usr/bin/perlmake && make testmake installperl -v# 只需要安装一下四个组件就可以了:# 因为具有依赖关系,所以需要按顺序执行:rpm -ivh mysql-community-common-.rpmrpm -ivh mysql-community-libs-.rpmrpm -ivh mysql-community-client-.rpmrpm -ivh mysql-community-server-*.rpm# 查看mysql是否启动service mysqld status# 启动mysqlservice mysqld start# 停止mysqlservice mysqld stop# 重启mysqlservice mysqld restart配置MySQL# 安装完成后,打印出的安装日志里面有一些有用的提示信息,如:# 查看临时密码:cat /root/.mysql_secret# /usr/bin/mysql_secure_installation# New default config file was created as /usr/my.cnf and# will be used by default by the server when you start it.# WARNING: Default config file /etc/my.cnf exists on the system# This file will be read by default by the MySQL server# If you do not want to use this, either remove it, or use the# –defaults-file argument to mysqld_safe when starting the server# 登录后修改密码:mysql> SET PASSWORD = PASSWORD(‘DBPwdStr’);# 为数据库创建访问账户,修改账户的限制IP,查询用户表:mysql> GRANT ALL ON . TO ‘username’@’%’ IDENTIFIED BY ‘DBPwdStr’ WITH GRANT OPTION;mysql> update mysql.user set host=’%’ where host=’::1’;mysql> delete from mysql.user where host<>’%’;mysql> select * from mysql.user \G;# 编辑MySQL的配置文件:vim /etc/my.cnf# 启动MySQL服务:service mysql restart && service mysql status# 启动错误# The server quit without updating PID file (/home/lnmp/mysql/data/localhost.localdomain.pid).# 1.可能是/usr/local/mysql/data/rekfan.pid文件没有写的权限# 解决方法 :给予权限,执行 “chown -R mysql:mysql /var/data” “chmod -R 755 /usr/local/mysql/data” 然后重新启动mysqld!# 2.可能进程里已经存在mysql进程# 解决方法:用命令“ps -ef|grep mysqld”查看是否有mysqld进程,如果有使用“kill -9 进程号”杀死,然后重新启动mysqld!# 3.可能是第二次在机器上安装mysql,有残余数据影响了服务的启动。# 解决方法:去mysql的数据目录/data看看,如果存在mysql-bin.index,将它删除。# 4.mysql在启动时没有指定配置文件时会使用/etc/my.cnf配置文件,查看该文件的[mysqld]下有没有指定的数据目录(datadir)。# 解决方法:请在[mysqld]下设置这一行:datadir = /usr/local/mysql/data安装计划任务管理工具 - crontabs 点击链接下载 crontabs-1.11-6.20121102git.el7.noarch.rpm,如需下载其他版本,请访问官网; 安装:rpm -ivh crontabs-1.11-6.20121102git.el7.noarch.rpm 添加自动备份任务,具体操作请参考MySQL的自动备份。下载(需要连接VPN)并安装HAProxy 点击链接下载 haproxy-1.5.19.tar.gz,如需下载其他版本请访问官网; 安装HAProxy:# 添加用户组和用户:groupadd -r haproxy useradd -g haproxy -M -s /sbin/nologin haproxy# 为安装文件夹授权:chown -R haproxy:haproxy /usr/local/haproxy# 查看内核:uname -r# 解压安装包tar -xvf haproxy-1.5.19.tar.gzcd haproxy-1.5.19# 根据内核版本进行编译(这里的版本对应的目标是linux310):make TARGET=linux310 ARCH=x86_64 PREFIX=/usr/local/haproxymake install PREFIX=/usr/local/# 将可执行文件拷贝到全局目录下:cp /usr/local/haproxy/sbin/haproxy /usr/sbin/haproxy配置HAProxy# 配置HAProxy;cat /etc/haproxy/haproxy.cfgvim /etc/haproxy/haproxy.cfg# 编辑配置文件内容,请参考HAProxy的负载均衡# 重启HAProxy服务;haproxy -f /etc/haproxy/haproxy.cfg# 测试HAProxy;ps -ef | grep haproxy# 访问HAProxy代理的地址和端口,分别停掉备服务器的Nginx服务后,继续访问正常则说明基本配置没问题;下载并安装Keepalived 点击链接下载Keepalived 1.4.5,下载其他版本请访问官网; 安装Keeplived;tar -xvf keepalived-1.4.5.tar.gzcd keepalived-1.4.5./configure –prefix=/usr/local/keepalivedmake && make installmkdir /etc/keepalivedcp /usr/local/keepalived/sbin/keepalived /usr/bin/keepalivedcp /usr/local/keepalived/etc/keepalived/keepalived.conf /etc/keepalived/keepalived.confcp /usr/local/keepalived/etc/sysconfig/keepalived /etc/sysconfig/keepalived# 设置开机启动chkconfig keepalived on# 服务操作命令service keepalived startservice keepalived stopservice keepalived restartservice keepalived statusservice keepalived restart & service keepalived status配置Keepalived;# 编辑配置文件:vim /etc/keepalived/keepalived.conf# 主从服务器的配置略有差别,具体配置请参照Keepalived的配置; ...

January 10, 2019 · 3 min · jiezi

使用Docker 一键部署 LNMP+Redis 环境

使用Docker 部署 LNMP+Redis 环境Docker 简介Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。容器是完全使用沙箱机制,相互之间不会有任何接口。推荐内核版本3.8及以上为什么使用Docker加速本地的开发和构建流程,容器可以在开发环境构建,然后轻松地提交到测试环境,并最终进入生产环境能够在让独立的服务或应用程序在不同的环境中得到相同的运行结果创建隔离的环境来进行测试高性能、超大规划的宿主机部署从头编译或者扩展现有的OpenShift或Cloud Foundry平台来搭建自己的PaaS环境目录安装Docker目录结构快速使用进入容器内部PHP扩展安装Composer安装常见问题处理常用命令Dockerfile语法docker-compose语法说明项目源码地址:GitHub安装Dockerwindows 安装参考macdocker toolbox参考 linux# 下载安装curl -sSL https://get.docker.com/ | sh# 设置开机自启sudo systemctl enable docker.servicesudo service docker start|restart|stop# 安装docker-composecurl -L https://github.com/docker/compose/releases/download/1.23.2/docker-compose-`uname -s-uname -m&gt; /usr/local/bin/docker-composechmod +x /usr/local/bin/docker-compose目录结构docker_lnmp├── v2├── mysql│ └── Dockerfile│ └── my.cnf├── nginx│ ├── Dockerfile│ ├── nginx.conf│ ├── log│ │ └── error.log│ └── www│ ├── index.html│ ├── index.php│ ├── db.php│ └── redis.php├── php│ ├── Dockerfile│ ├── www.conf│ ├── php-fpm.conf│ ├── php.ini│ └── log│ └── php-fpm.log└── redis └── Dockerfile └── redis.conf创建镜像与安装直接使用docker-compose一键制作镜像并启动容器版本一该版本是通过拉取纯净的CentOS镜像,通过Dockerfile相关命令进行源码编译安装各个服务。所以该方式很方便定制自己需要的镜像,但是占用空间大且构建慢。git clone https://github.com/voocel/docker-lnmp.gitcd docker-lnmpdocker-compose up -d版本二(推荐)git clone https://github.com/voocel/docker-lnmp.gitcd docker-lnmp/v2chmod 777 ./redis/redis.logchmod -R 777 ./redis/datadocker-compose up -d该版本是通过拉取官方已经制作好的各个服务的镜像,再通过Dockerfile相关命令根据自身需求做相应的调整。所以该方式构建迅速使用方便,因为是基于Alpine Linux所以占用空间很小。测试使用docker ps查看容器启动状态,若全部正常启动了则通过访问127.0.0.1、127.0.0.1/index.php、127.0.0.1/db.php、127.0.0.1/redis.php 即可完成测试(若想使用https则请修改nginx下的dockerfile,和nginx.conf按提示去掉注释即可,灵需要在ssl文件夹中加入自己的证书文件,本项目自带的是空的,需要自己替换,保持文件名一致)进入容器内部使用 docker execdocker exec -it ngixn /bin/sh使用nsenter命令# cd /tmp; curl https://www.kernel.org/pub/linux/utils/util-linux/v2.24/util-linux-2.24.tar.gz | tar -zxf-; cd util-linux-2.24;# ./configure --without-ncurses# make nsenter &amp;&amp; sudo cp nsenter /usr/local/bin为了连接到容器,你还需要找到容器的第一个进程的 PID,可以通过下面的命令获取再执行。PID=$(docker inspect --format "{{ .State.Pid }}" container_id)# nsenter --target $PID --mount --uts --ipc --net --pidPHP扩展安装安装PHP官方源码包里的扩展(如:同时安装pdo_mysql mysqli pcntl gd四个个扩展)在php的Dockerfile中加入以下命令RUN apk add libpng-dev \ &amp;&amp; docker-php-ext-install pdo_mysql mysqli pcntl gd \注:因为该镜像缺少gd库所需的libpng-dev包,所以需要先下载这个包PECL 扩展安装# 安装扩展RUN pecl install memcached-2.2.0 \ # 启用扩展 &amp;&amp; docker-php-ext-enable memcached \通过下载扩展源码,编译安装的方式安装# 安装Redis和swoole扩展RUN cd ~ \ &amp;&amp; wget https://github.com/phpredis/phpredis/archive/4.2.0.tar.gz \ &amp;&amp; tar -zxvf 4.2.0.tar.gz \ &amp;&amp; mkdir -p /usr/src/php/ext \ &amp;&amp; mv phpredis-4.2.0 /usr/src/php/ext/redis \ &amp;&amp; docker-php-ext-install redis \ &amp;&amp; apk add libstdc++\ &amp;&amp; cd ~ \ &amp;&amp; wget https://github.com/swoole/swoole-src/archive/v4.2.12.tar.gz \ &amp;&amp; tar -zxvf v4.2.12.tar.gz \ &amp;&amp; mkdir -p /usr/src/php/ext \ &amp;&amp; mv swoole-src-4.2.12 /usr/src/php/ext/swoole \ &amp;&amp; docker-php-ext-install swoole \注:因为该镜像需要先安装swoole依赖的libstdc++,否则安装成功后无法正常加载swoole扩展Composer安装在Dockerfile中加入RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/bin/ --filename=composer \常见问题处理redis启动失败问题在v2版本中redis的启动用户为redis不是root,所以在宿主机中挂载的./redis/redis.log和./redis/data需要有写入权限。 chmod 777 ./redis/redis.log chmod 777 ./redis/dataMYSQL连接失败问题在v2版本中是最新的MySQL8,而该版本的密码认证方式为Caching_sha2_password,而低版本的php和mysql可视化工具可能不支持,可通过phpinfo里的mysqlnd的Loaded plugins查看是否支持该认证方式,否则需要修改为原来的认证方式mysql_native_password: select user,host,plugin,authentication_string from mysql.user; ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '123456'; FLUSH PRIVILEGES;注意挂载目录的权限问题,不然容器成功启动几秒后立刻关闭,例:以下/data/run/mysql 目录没权限的情况下就会出现刚才那种情况docker run --name mysql57 -d -p 3306:3306 -v /data/mysql:/var/lib/mysql -v /data/logs/mysql:/var/log/mysql -v /data/run/mysql:/var/run/mysqld -e MYSQL_ROOT_PASSWORD=123456 -it centos/mysql:v5.7需要注意php.ini 中的目录对应 mysql 的配置的目录需要挂载才能获取文件内容,不然php连接mysql失败# php.inimysql.default_socket = /data/run/mysql/mysqld.sockmysqli.default_socket = /data/run/mysql/mysqld.sockpdo_mysql.default_socket = /data/run/mysql/mysqld.sock# mysqld.cnfpid-file = /var/run/mysqld/mysqld.pidsocket = /var/run/mysqld/mysqld.sock使用php连接不上redis# 错误的$redis = new Redis;$rs = $redis-&gt;connect('127.0.0.1', 6379);php连接不上,查看错误日志PHP Fatal error: Uncaught RedisException: Redis server went away in /www/index.php:7考虑到docker 之间的通信应该不可以用127.0.0.1 应该使用容器里面的ip,所以查看redis 容器的ip[root@localhost docker]# docker psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESb5f7dcecff4c docker_nginx "/usr/sbin/nginx -..." 4 seconds ago Up 3 seconds 0.0.0.0:80-&gt;80/tcp, 0.0.0.0:443-&gt;443/tcp nginx60fd2df36d0e docker_php "/usr/local/php/sb..." 7 seconds ago Up 5 seconds 9000/tcp php7c7df6f8eb91 hub.c.163.com/library/mysql:latest "docker-entrypoint..." 12 seconds ago Up 11 seconds 3306/tcp mysqla0ebd39f0f64 docker_redis "usr/local/redis/s..." 13 seconds ago Up 12 seconds 6379/tcp redis注意测试的时候连接地址需要容器的ip或者容器名names,比如redis、mysql.例如nginx配置php文件解析 fastcgi_pass php:9000;例如php连接redis $redis = new Redis;$res = $redis-&gt;connect('redis', 6379);因为容器ip是动态的,重启之后就会变化,所以可以创建静态ip第一步:创建自定义网络#备注:这里选取了172.172.0.0网段,也可以指定其他任意空闲的网段docker network create --subnet=172.171.0.0/16 docker-atdocker run --name redis326 --net docker-at --ip 172.171.0.20 -d -p 6379:6379 -v /data:/data -it centos/redis:v3.2.6连接redis 就可以配置对应的ip地址了,连接成功$redis = new Redis;$rs = $redis-&gt;connect('172.171.0.20', 6379);另外还有种可能phpredis连接不上redis,需要把redis.conf配置略作修改。bind 127.0.0.1改为:bind 0.0.0.0启动docker web服务时 虚拟机端口转发 外部无法访问 一般出现在yum update的时候(WARNING: IPv4 forwarding is disabled. Networking will not work.)或者宿主机可以访问,但外部无法访问vi /etc/sysctl.conf或者vi /usr/lib/sysctl.d/00-system.conf添加如下代码: net.ipv4.ip_forward=1重启network服务systemctl restart network查看是否修改成功sysctl net.ipv4.ip_forward如果返回为"net.ipv4.ip_forward = 1"则表示成功了如果使用最新的MySQL8无法正常连接,由于最新版本的密码加密方式改变,导致无法远程连接。# 修改密码加密方式ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '123456';常用命令docker start 容器名(容器ID也可以)docker stop 容器名(容器ID也可以)docker run 命令加 -d 参数,docker 会将容器放到后台运行docker ps 正在运行的容器docker logs --tail 10 -tf 容器名 查看容器的日志文件,加-t是加上时间戳,f是跟踪某个容器的最新日志而不必读整个日志文件docker top 容器名 查看容器内部运行的进程docker exec -d 容器名 touch /etc/new_config_file 通过后台命令创建一个空文件docker run --restart=always --name 容器名 -d ubuntu /bin/sh -c "while true;do echo hello world; sleep 1; done" 无论退出代码是什么,docker都会自动重启容器,可以设置 --restart=on-failure:5 自动重启的次数docker inspect 容器名 对容器进行详细的检查,可以加 --format='{(.State.Running)}' 来获取指定的信息docker rm 容器ID 删除容器,注,运行中的容器无法删除docker rm $(docker ps -aq) 删除所有容器docker rmi $(docker images -aq) 删除所有镜像docker images 列出镜像docker pull 镜像名:标签 拉镜像docker search 查找docker Hub 上公共的可用镜像docker build -t='AT/web_server:v1' 命令后面可以直接加上github仓库的要目录下存在的Dockerfile文件。 命令是编写Dockerfile 之后使用的。-t选项为新镜像设置了仓库和名称:标签docker login 登陆到Docker Hub,个人认证信息将会保存到$HOME/.dockercfg,docker commit -m="comment " --author="AT" 容器ID 镜像的用户名/仓库名:标签 不推荐这种方法,推荐dockerfiledocker history 镜像ID 深入探求镜像是如何构建出来的docker port 镜像ID 端口 查看映射情况的容器的ID和容器的端口号,假设查询80端口对应的映射的端口run 运行一个容器, -p 8080:80 将容器内的80端口映射到docker宿主机的某一特定端口,将容器的80端口绑定到宿主机的8080端口,另 127.0.0.1:80:80 是将容器的80端口绑定到宿主机这个IP的80端口上,-P 是将容器内的80端口对本地的宿主机公开http://docs.docker.com/refere... 查看更多的命令docker push 镜像名 将镜像推送到 Docker Hubdocker rmi 镜像名 删除镜像docker attach 容器ID 进入容器docker network create --subnet=172.171.0.0/16 docker-at 选取172.172.0.0网段docker build 就可以加 -ip指定容器ip 172.171.0.10 了删除所有容器和镜像的命令docker rmdocker ps -a |awk ‘{print $1}’ | grep [0-9a-z]` 删除停止的容器docker rmi $(docker images | awk ‘/^<none>/ { print $3 }’)Dockerfile语法MAINTAINER 标识镜像的作者和联系方式EXPOSE 可以指定多个EXPOSE向外部公开多个端口,可以帮助多个容器链接FROM 指令指定一个已经存在的镜像#号代表注释RUN 运行命令,会在shell 里使用命令包装器 /bin/sh -c 来执行。如果是在一个不支持shell 的平台上运行或者不希望在shell 中运行,也可以 使用exec 格式 的RUN指令ENV REFRESHED_AT 环境变量 这个环境亦是用来表明镜像模板最后的更新时间VOLUME 容器添加卷。一个卷是可以 存在于一个或多个容器内的特定的目录,对卷的修改是立刻生效的,对卷的修改不会对更新镜像产品影响,例:VOLUME["/opt/project","/data"]ADD 将构建环境 下的文件 和目录复制到镜像 中。例 ADD nginx.conf /conf/nginx.conf 也可以是取url 的地址文件,如果是压缩包,ADD命令会自动解压、USER 指定镜像用那个USER 去运行COPY 是复制本地文件,而不会去做文件提取(解压包不会自动解压) 例:COPY conf.d/ /etc/apache2/ 将本地conf.d目录中的文件复制到/etc/apache2/目录中docker-compose.yml 语法说明image 指定为镜像名称或镜像ID。如果镜像不存在,Compose将尝试从互联网拉取这个镜像build 指定Dockerfile所在文件夹的路径。Compose将会利用他自动构建这个镜像,然后使用这个镜像command 覆盖容器启动后默认执行的命令links 链接到其他服务容器,使用服务名称(同时作为别名)或服务别名(SERVICE:ALIAS)都可以external_links 链接到docker-compose.yml外部的容器,甚至并非是Compose管理的容器。参数格式和links类似ports 暴露端口信息。宿主机器端口:容器端口(HOST:CONTAINER)格式或者仅仅指定容器的端口(宿主机器将会随机分配端口)都可以(注意:当使用 HOST:CONTAINER 格式来映射端口时,如果你使用的容器端口小于 60 你可能会得到错误得结果,因为 YAML 将会解析 xx:yy 这种数字格式为 60 进制。所以建议采用字符串格式。)expose 暴露端口,与posts不同的是expose只可以暴露端口而不能映射到主机,只供外部服务连接使用;仅可以指定内部端口为参数volumes 设置卷挂载的路径。可以设置宿主机路径:容器路径(host:container)或加上访问模式(host:container:ro)ro就是readonly的意思,只读模式volunes_from 挂载另一个服务或容器的所有数据卷environment 设置环境变量。可以属于数组或字典两种格式。如果只给定变量的名称则会自动加载它在Compose主机上的值,可以用来防止泄露不必要的数据env_file 从文件中获取环境变量,可以为单独的文件路径或列表。如果通过docker-compose -f FILE指定了模板文件,则env_file中路径会基于模板文件路径。如果有变量名称与environment指令冲突,则以后者为准(环境变量文件中每一行都必须有注释,支持#开头的注释行)extends 基于已有的服务进行服务扩展。例如我们已经有了一个webapp服务,模板文件为common.yml。编写一个新的 development.yml 文件,使用 common.yml 中的 webapp 服务进行扩展。后者会自动继承common.yml中的webapp服务及相关的环境变量net 设置网络模式。使用和docker client 的 –net 参数一样的值pid 和宿主机系统共享进程命名空间,打开该选项的容器可以相互通过进程id来访问和操作dns 配置DNS服务器。可以是一个值,也可以是一个列表cap_add,cap_drop 添加或放弃容器的Linux能力(Capability)dns_search 配置DNS搜索域。可以是一个值也可以是一个列表注意:使用compose对Docker容器进行编排管理时,需要编写docker-compose.yml文件,初次编写时,容易遇到一些比较低级的问题,导致执行docker-compose up时先解析yml文件的错误。比较常见的是yml对缩进的严格要求。yml文件还行后的缩进,不允许使用tab键字符,只能使用空格,而空格的数量也有要求,一般两个空格。项目源码地址:GitHub ...

January 10, 2019 · 3 min · jiezi

做了一个技术博客聚合站,大家来提交自己的博客鸭

抛 https://www.kewangst.com/提交网站说明 https://www.kewangst.com/page…友链说明 https://www.kewangst.com/page…

January 9, 2019 · 1 min · jiezi

网络协议 20 - RPC 协议(上)- 基于XML的SOAP协议

【前五篇】系列文章传送门:网络协议 15 - P2P 协议:小种子大学问网络协议 16 - DNS 协议:网络世界的地址簿网络协议 17 - HTTPDNS:私人定制的 DNS 服务网络协议 18 - CDN:家门口的小卖铺网络协议 19 - RPC 协议综述:远在天边,近在眼前 上一节我们了解 RPC 的经典模型和设计要点,并用最早期的 ONC RPC 为例子,详述了具体的实现。而时代在进步,ONC RPC 逐渐因为各种问题被替代,SOAP 协议就是替代者之一。ONC RPC 存在的问题 ONC RPC 将客户端要发送的参数,以及服务端要发送的回复,都压缩为一个二进制串,这样固然能够解决双方的协议约定问题,但是存在一定的不方便。 首先,需要双方的压缩格式完全一致,一点都不能差。一旦有少许的差错,多一位,少一位或者错一位,都可能造成无法解压缩。当然,我们可以用传输层的可靠性以及加入校验值等方式,来减少传输过程中的差错。 其次,协议修改不灵活。如果不是传输过程中造成的差错,而是客户端因为业务逻辑的改变,添加或者删除了字段,或者服务端添加或者删除了字段,而双方没有及时通知,或者线上系统没有及时升级,就会造成解压缩不成功。 因而,当业务发生改变,需要多传输一些参数或者少传输一些参数的时候,都需要及时通知对方,并且根据约定好的协议文件重新生成双方的 Stub 程序。自然,这样灵活性比较差。 如果仅仅是沟通的问题也还好解决,其实更难弄的还有版本的问题。比如在服务端提供一个服务,参数的格式是版本一的,已经有 50 个客户端在线上调用了。现在有一个客户端有个需求,要加一个字段,怎么办呢?这可是一个大工程,所有的客户端都要适配这个,需要重新写程序,加上这个字段,但是传输值是 0,不需要这个字段的客户端很“冤”,本来没我啥事儿,为啥让我也忙活? 最后,ONC RPC 的设计明显是面向函数的,而非面向对象。而当前面向对象的业务逻辑设计与实现方式已经成为主流。 这一切的根源就在于压缩。这就像平时我们爱用缩略语。如果是篮球爱好者,你直接说 NBA,他马上就知道什么意思,但是如果你给一个大妈说 NBA,她可能就不知所云。 所以,这种 RPC 框架只能用于客户端和服务端全由一拨人开发的场景,或者至少客户端和服务端的开发人员要密切沟通,相互合作,有大量的共同语言,才能按照既定的协议顺畅地进行工作。XML 与 SOAP 但是,一般情况下,我们做一个服务,都是要提供给陌生人用的,你和客户不会经常沟通,也没有什么共同语言。就像你给别人介绍 NBA,你要说美国职业篮球赛,这样不管他是干啥的,都能听得懂。 放到我们的场景中,对应的就是用文本类的方式进行传输。无论哪个客户端获得这个文本,都能够知道它的意义。 一种常见的文本类格式是 XML。我们这里举个例子来看。<?xml version=“1.0” encoding=“UTF-8”?><cnblog:purchaseOrder xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xmlns:cnblog=“http://www.example.com”> <order> <date>2019-01-08</date> <className> 板栗焖鸡 </className> <price>58</price> </order></cnblog:purchaseOrder> 我这里不准备详细讲述 XML 的语法规则,但是你相信我,看完下面的内容,即便你没有学过 XML,也能一看就懂,这段 XML 描述的是什么,不像全面的二进制,你看到的都是 010101,不知所云。 有了这个,刚才我们说的那几个问题就都不是问题了。 首先,格式没必要完全一致。比如如果我们把 price 和 author 换个位置,并不影响客户端和服务端解析这个文本,也根本不会误会,说这个作者的名字叫 68。 如果有的客户端想增加一个字段,例如添加一个推荐人字段,只需要在上面的文件中加一行:<recommended> Gary </recommended> 对于不需要这个字段的客户端,只要不解析这一行就是了。只要用简单的处理,就不会出现错误。 另外,这种表述方式显然是描述一个订单对象的,是一种面向对象的、更加接近用户场景的表示方式。 既然 XML 这么好,接下来我们来看看怎么把它用在 RPC 中。传输协议问题 我们先解决第一个,传输协议的问题。 基于 XML 的最著名的通信协议就是SOAP了,全称简单对象访问协议(Simple Object Access Protocol)。它使用 XML 编写简单的请求和回复消息,并用 HTTP 协议进行传输。 SOAP 将请求和回复放在一个信封里面,就像传递一个邮件一样。信封里面的信分抬头和正文POST /purchaseOrder HTTP/1.1Host: www.cnblog.comContent-Type: application/soap+xml; charset=utf-8Content-Length: nnn<?xml version=“1.0”?><soap:Envelope xmlns:soap=“http://www.w3.org/2001/12/soap-envelope"soap:encodingStyle="http://www.w3.org/2001/12/soap-encoding"> <soap:Header> <m:Trans xmlns:m=“http://www.w3schools.com/transaction/" soap:mustUnderstand=“1”>1234 </m:Trans> </soap:Header> <soap:Body xmlns:m=“http://www.cnblog.com/perchaseOrder"> <m:purchaseOrder”> <order> <date>2019-01-08</date> <className> 板栗焖鸡 </className> <price>88</price> </order> </m:purchaseOrder> </soap:Body></soap:Envelope> HTTP 协议我们学过,这个请求使用 POST 方法,发送一个格式为 application/soap + xml 的 XML 正文给 www.geektime.com ,从而下一个单,这个订单封装在 SOAP 的信封里面,并且表明这是一笔交易(transaction),而且订单的详情都已经写明了。协议约定问题 接下来我们解决第二个问题,就是双方的协议约定是什么样的? 因为服务开发出来是给陌生人用的,就像上面下单的那个 XML 文件,对于客户端来说,它如何知道应该拼装成上面的格式呢?这就需要对于服务进行描述,因为调用的人不认识你,所以没办法找到你,问你的服务应该如何调用。 当然你可以写文档,然后放在官方网站上,但是你的文档不一定更新得那么及时,而且你也写的文档也不一定那么严谨,所以常常会有调试不成功的情况。因而,我们需要一种相对比较严谨的Web 服务描述语言,WSDL(Web Service Description Languages)。它也是一个 XML 文件。 在这个文件中,要定义一个类型 order,与上面的 XML 对应起来。 <wsdl:types> <xsd:schema targetNamespace=“http://www.example.org/cnblog"> <xsd:complexType name=“order”> <xsd:element name=“date” type=“xsd:string”></xsd:element><xsd:element name=“className” type=“xsd:string”></xsd:element><xsd:element name=“Author” type=“xsd:string”></xsd:element> <xsd:element name=“price” type=“xsd:int”></xsd:element> </xsd:complexType> </xsd:schema> </wsdl:types> 接下来,需要定义一个 message 的结构。 <wsdl:message name=“purchase”> <wsdl:part name=“purchaseOrder” element=“tns:order”></wsdl:part> </wsdl:message> 接下来,应该暴露一个端口。 <wsdl:portType name=“PurchaseOrderService”> <wsdl:operation name=“purchase”> <wsdl:input message=“tns:purchase”></wsdl:input> <wsdl:output message=”……"></wsdl:output> </wsdl:operation> </wsdl:portType> 然后,我们来编写一个 binding,将上面定义的信息绑定到 SOAP 请求的 body 里面。 <wsdl:binding name=“purchaseOrderServiceSOAP” type=“tns:PurchaseOrderService”> <soap:binding style=“rpc” transport=“http://schemas.xmlsoap.org/soap/http" /> <wsdl:operation name=“purchase”> <wsdl:input> <soap:body use=“literal” /> </wsdl:input> <wsdl:output> <soap:body use=“literal” /> </wsdl:output> </wsdl:operation> </wsdl:binding> 最后,我们需要编写 service。 <wsdl:service name=“PurchaseOrderServiceImplService”> <wsdl:port binding=“tns:purchaseOrderServiceSOAP” name=“PurchaseOrderServiceImplPort”> <soap:address location=“http://www.cnblog.com:8080/purchaseOrder" /> </wsdl:port> </wsdl:service> WSDL 还是有些复杂的,不过好在有工具可以生成。 对于某个服务,哪怕是一个陌生人,都可以通过在服务地址后面加上“?wsdl”来获取到这个文件,但是这个文件还是比较复杂,比较难以看懂。不过好在也有工具可以根据 WSDL 生成客户端 Stub,让客户端通过 Stub 进行远程调用,就跟调用本地的方法一样。服务发现问题 最后解决第三个问题,服务发现问题。 这里有一个UDDI(Universal Description, Discovery, and Integration),也即统一描述、发现和集成协议。它其实是一个注册中心,服务提供方可以将上面的 WSDL 描述文件,发布到这个注册中心,注册完毕后,服务使用方可以查找到服务的描述,封装为本地的客户端进行调用。小结原来的二进制 RPC 有很多缺点,格式要求严格,修改过于复杂,不面向对象,于是产生了基于文本的调用方式——基于 XML 的 SOAP;SOAP 有三大要素:协议约定用 WSDL、传输协议用 HTTP、服务发现用 UDDL。 ...

January 9, 2019 · 2 min · jiezi

【前端词典】代理的概念及其应用

前言在平时的工作中,总是会遇到代理的概念。之前我只知道有代理这个概念,不过对其没有一个清晰的理解。于是带着以下两个问题开始学习正向代理以及反向代理。什么是正向代理,什么是反向代理正向代理可以做什么,反向代理可以做什么概念首先附上一张说明图,先有一个整体的理解。正向代理( Forward Proxy ):是指是一个位于客户端和原始服务器之间的服务器,为了从原始服务器取得内容, 客户端向代理发送一个请求并指定目标(原始服务器),然后代理向原始服务器转交请求并将获得的内容返回给客户端。客户端才能使用正向代理。 反向代理( Reverse Proxy ):是指以代理服务器来接受 Internet 上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给 Internet 上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。特点正向代理代理客户;隐藏真实的客户,为客户端收发请求,使真实客户端对服务器不可见;一个局域网内的所有用户可能被一台服务器做了正向代理,由该台服务器负责 HTTP 请求;意味着同服务器做通信的是正向代理服务器;反向代理代理服务器;隐藏了真实的服务器,为服务器收发请求,使真实服务器对客户端不可见;负载均衡服务器,将用户的请求分发到空闲的服务器上;意味着用户和负载均衡服务器直接通信,即用户解析服务器域名时得到的是负载均衡服务器的 IP ;共同点都是做为服务器和客户端的中间层都可以加强内网的安全性,阻止 web 攻击都可以做缓存机制实际应用Nginx 服务器Nginx 服务器的功能有很多,诸如反向代理、负载均衡、静态资源服务器等。客户端本来可以直接通过 HTTP 协议访问服务器,不过我们可以在中间加上一个 Nginx 服务器,客户端请求 Nginx 服务器,Nginx 服务器请求应用服务器,然后将结果返回给客户端,此时 Nginx 服务器就是反向代理服务器。在虚拟主机的配置中配置反向代理# 虚拟主机的配置server { listen 8080; # 监听的端口 server_name 192.168.1.1; # 配置访问域名 root /data/toor; # 站点根目录 error_page 502 404 /page/404.html; # 错误页面 location ^~ /api/ { # 使用 /api/ 代理 proxy_pass 的值 proxy_pass http://192.168.20.1:8080; # 被代理的应用服务器 HTTP 地址 }}以上简单的配置就可以实现反向代理的功能。当然反向代理也可以处理跨域问题,在 Vue 中就可以使用 proxyTable 这个属性进行相关的配置来解决跨域问题带来的烦恼。配置如下:…proxyTable: { ‘/weixin’: { target: ‘http://192.168.48.11:8100/’, // 接口的域名 secure: false, // 如果是 https 接口,需要配置这个参数 changeOrigin: true, // 如果接口跨域,需要进行这个参数配置 pathRewrite: { ‘^/weixin’: ’’ } },},…负载均衡的配置# upstream 表示负载服务器池,定义名字为 myupstream my { server 192.168.2.1:8080 weight=1 max_fails=2 fail_timeout=30s; server 192.168.2.2:8080 weight=1 max_fails=2 fail_timeout=30s; server 192.168.2.3:8080 weight=1 max_fails=2 fail_timeout=30s; server 192.168.2.4:8080 weight=1 max_fails=2 fail_timeout=30s; # 即在 30s 内尝试 2 次失败即认为主机不可用 }负载均衡即将 请求/数据 轮询分摊到多个服务器上执行,负载均衡的关键在于 均匀。也可以通过 ip-hash 的方式,根据客户端 ip 地址的 hash 值将请求分配给固定的某一个服务器处理。另外,服务器的硬件配置可能不同,配置好的服务器可以处理更多的请求,这时可以通过 weight 参数来控制。以上。前端词典系列本文是《前端词典》系列的第一篇文章,这个系列会持续更新,每一期我都会讲一个出现频率较高的知识点。希望大家在阅读的过程当中可以斧正文中出现不严谨或是错误的地方,本人将不胜感激;若通过本系列而有所得,本人亦将不胜欣喜。内容: 前端以及网络相关知识点的介绍并加以实际应用作为辅助。目的: 这个系列的文章可以对读者起到一点帮助,解开一些迷惑。希望各位多指点一二,不吝赐教。传送门【前端词典】代理的概念及其应用【前端词典】滚动穿透问题的解决方案 ...

January 8, 2019 · 1 min · jiezi

Thinkphp在Nginx服务器下部署的问题--宝塔面板篇

最近我把Thinkphp5的项目部署到服务器遇到个问题,Nginx服务器下访问时404,在网上看到很多方法都是修改Nginx配置的教程,非常繁琐,我照着修改了也没弄好。我用的是宝塔面板,其实宝塔已经配置好了解决方案。① 打开你的网站② 修改配置如下就好啦

January 7, 2019 · 1 min · jiezi

centos7 安装nginx并配置代理

前言笔者在国外租了一个虚机,用来部署自己的博客应用,并申请了一个域名51think.net来指向这个虚机。随着部署的应用越来越多,而80端口只有一个,无法直接通过域名去访问不同的应用。由此而来,部署一个代理服务器势在必行。本文对nginx的安装和配置进行简单整理,希望对初学者有帮助。一、安装nginx安装有两种方式,即yum和wget。1、通过yum方式在线安装需要注意的一点是,nginx并不在yum的安装源中。什么是yum?你可以理解为一个rpm包管理器的前置(什么是rpm?自己百度吧。。),yum类似于maven的效果,给一个包名,就能将其所依赖的软件包全部下载下来。maven是有中央仓库的,即包的来源。yum也是同样的概念,它也需要一个包源,而且可以配置多个,这个源可以是本地的也可以是网络的,而nginx并不在它的源中,因此我们要把它加到yum的源中。执行如下命令:rpm -ivh http://nginx.org/packages/centos/7/noarch/RPMS/nginx-release-centos-7-0.el7.ngx.noarch.rpm这个操作并不是安装nginx,只是安装了一个nginx的源。执行完成之后,会在/etc/yum.repos.d目录中看到多了一个文件nginx.repo 。从这个文件的后缀我们可以感知到,.repo即repository,仓库配置。文件内容如下:核心要素也就是一个网址。即告知yum命令,可以从这个网址里找nginx下载并安装。现在开始真正的安装,执行命令yum install -y nginx即可。2、通过wget下载nginx的压缩包wget http://nginx.org/download/nginx-1.10.1.tar.gz解压tar -zxvf nginx-1.10.1.tar.gz,我本地的解压缩目录是/usr/local/,这时候我们可以启动一下nginx观察一下效果,到/usr/local/nginx/sbin目录,执行./nginx,然后在浏览器中访问http://localhost ,弹出以下页面则表示安装成功(确保80端口没有被占用):二、配置代理1、单点代理配置在虚机上找到nginx的安装目录,找到nginx.conf文件。笔者的文件路径是:/usr/local/nginx/conf/nginx.conf这个配置文件的内容很简单,结构类似于json,重点关注server领域的配置,其他配置项默认即可。笔者的配置如下,供参考: server { listen 80; server_name www.51think.net 51think.net www.ueasy.cc; #charset koi8-r; #access_log logs/host.access.log main; #blog location / { proxy_pass http://138.128.193.108:8080; } #mall location /wx { proxy_pass http://138.128.193.108:8081; } }listen表示监听的端口,http的是80,https的是443。server_name表示本配置项是为哪些域名准备的,即可以接受哪些域名的访问。location就是代理的配置了,/表示可以通过域名的根目录去访问http://138.128.193.108:8080的tomcat服务,/wx表示可以通过“域名+/wx”的形式去访问http://138.128.193.108:8081的tomcat服务。要注意的一点是,如果location作为tomcat服务的全局入口,那么location的路径需要和tomcat的contextPath保持一致,否则访问可能出现404错误。举例说明,如果我的博客网站tomcat的contextPath是/blog,即直接访问路径应该是http://138.128.193.108:8080/blog。如果在nginx层面将location配置成如下: location / { proxy_pass http://138.128.193.108:8080; } 则通过域名http://51think.net/这样访问时,将会被代理到http://138.128.193.108:8080/这个访问路径,显然这样是访问不通的。如果tomcat的contextPath是/则没问题。2、负载均衡代理配置负载均衡配置也比较简单,将上文location配置中的 IP+端口换成一个新的配置项,然后在新的配置项里加入我们要负载的节点和负载的策略。location / { #将ip和端口信息换成一个新的配置项manyserver(自由命名) proxy_pass http://manyserver; }配置manyserver:upstream manyserver{ server 138.128.193.108:8080; #tomcat server 138.128.193.108:8084; #tomcat server 138.128.193.108:8085; #tomcat }upstream manyserver配置项里,我们还可以制定负载均衡策略,比如iphash,权重,轮询等,在此不再赘述。以上就是nginx安装配置的全部内容,希望对初学者有所帮助。三、注意事项1、nginx.conf中可以配置多个server节点,nginx可以根据监听端口或者访问域名去定位到不同的server配置项。2、配置完成之后,记得要重启nginx。到nginx的安装目录/usr/local/nginx/sbin/,执行./nginx -s reload即可。

January 6, 2019 · 1 min · jiezi

微服务实战 - docker-compose实现mysql+springboot+angular

前言这是一次完整的项目实践,Angular页面+Springboot接口+MySQL都通过Dockerfile打包成docker镜像,通过docker-compose做统一编排。目的是实现整个项目产品的轻量级和灵活性,在将各个模块的镜像都上传公共镜像仓库后,任何人都可以通过 “docker-compose up -d” 一行命令,将整个项目的前端、后端、数据库以及文件服务器等,运行在自己的服务器上。本项目是开发一个类似于segmentfault的文章共享社区,我的设想是当部署在个人服务器上时就是个人的文章库,部署在项目组的服务器上就是项目内部的文章库,部署在公司的服务器上就是所有职工的文章共享社区。突出的特点就是,项目相关的所有应用和文件资源都是灵活的,用户可以傻瓜式地部署并使用,对宿主机没有任何依赖。目前一共有三个docker镜像,考虑以后打在一个镜像中,但目前只能通过docker-compose来编排这三个镜像。MySQL镜像:以MySQL为基础,将项目所用到的数据库、表结构以及一些基础表的数据库,通过SQL脚本打包在镜像中。用户在启动镜像后就自动创建了项目所有的数据库、表和基础表数据。SpringBoot镜像:后台接口通过SpringBoot开发,开发完成后直接可以打成镜像,由于其内置tomcat,可以直接运行,数据库指向启动好的MySQL容器中的数据库。Nginx(Angular)镜像:Nginx镜像中打包了Angular项目的dist目录资源,以及default.conf文件。主要的作用有:部署Angular项目页面;挂载宿主机目录作为文件服务器;以及反向代理SpringBoot接口,解决跨域问题等等。最后三个docker容器的编排通过docker-compose来实现,三个容器之间的相互访问都通过容器内部的别名,避免了宿主机迁移时ip无法对应的问题。为了方便开发,顺便配了个自动部署。MySQL镜像初始化脚本在项目完成后,需要生成项目所需数据库、表结构以及基础表数据的脚本,保证在运行该docker容器中,启动MySQL数据库时,自动构建数据库和表结构,并初始化基础表数据。Navicat for MySQL的客户端支持导出数据库的表结构和表数据的SQL脚本。如果没有安装Navicat,可以在连接上容器中开发用的MySQL数据库,通过mysqldump 命令导出数据库表结构和数据的SQL脚本。下文中就是将数据库的SQL脚本导出到宿主机的/bees/sql 目录:docker exec -it mysql mysqldump -uroot -pPASSWORD 数据库名称 > /bees/sql/数据库名称.sql以上只是导出 表结构和表数据的脚本,还要在SQL脚本最上方加上 生成数据库的SQL:drop database if exists 数据库名称;create database 数据库名称;use 数据库名称;通过以上两个步骤,数据库、表结构和表数据三者的初始化SQL脚本就生成好了。Dockerfile构建镜像我们生成的SQL脚本叫 bees.sql,在MySQL官方镜像中提供了容器启动时自动执行/docker-entrypoint-initdb.d文件夹下的脚本的功能(包括shell脚本和sql脚本),我们在后续生成镜像的时候,将上述生成的SQL脚本COPY到MySQL的/docker-entrypoint-initdb.d目录下就可以了。现在我们写Dockerfile,很简单:FROM mysqlMAINTAINER kerry(kerry.wu@definesys.com)COPY bees.sql /docker-entrypoint-initdb.d将 bees.sql 和 Dockerfile 两个文件放在同一目录,执行构建镜像的命令就可以了:docker build -t bees-mysql .现在通过 docker images,就能看到本地的镜像库中就已经新建了一个 bees-mysql的镜像啦。SpringBoot镜像springboot构建镜像的方式很多,有通过代码生成镜像的,也有通过jar包生成镜像的。我不想对代码有任何污染,就选择后者,通过生成的jar包构建镜像。创建一个目录,上传已经准备好的springboot的jar包,这里名为bees-0.0.1-SNAPSHOT.jar,然后同样编写Dockerfile文件:FROM java:8VOLUME /tmpADD bees-0.0.1-SNAPSHOT.jar /bees-springboot.jarEXPOSE 8010ENTRYPOINT [“java”,"-Djava.security.egd=file:/dev/./urandom","-jar","-Denv=DEV","/bees-springboot.jar"]将bees-0.0.1-SNAPSHOT.jar和Dockerfile放在同一目录执行命令开始构建镜像,同样在本地镜像库中就生成了bees-springboot的镜像:docker build -t bees-springboot .Nginx(Angular)镜像Nginx的配置该镜像主要在于nginx上conf.d/default.conf文件的配置,主要实现三个需求:1、Angualr部署Angular的部署很简单,只要将Angular项目通过 ng build –prod 命令生成dist目录,将dist目录作为静态资源文件放在服务器上访问就行。我们这里就把dist目录打包在nginx容器中,在default.conf上配置访问。2、文件服务器项目为文章共享社区,少不了的就是一个存储文章的文件服务器,包括存储一些图片之类的静态资源。需要在容器中创建一个文件目录,通过default.conf上的配置将该目录代理出来,可以直接访问目录中的文件。当然为了不丢失,这些文件最好是保存在宿主机上,在启动容器时可以将宿主机本地的目录挂载到容器中的文件目录。3、接口跨域问题在前后端分离开发的项目中,“跨域问题”是较为常见的,SpringBoot的容器和Angular所在的容器不在同一个ip和端口,我们同样可以在default.conf上配置反向代理,将后台接口代理成同一个ip和端口的地址。话不多说,结合上面三个问题,我们最终的default.conf为:server { listen 80; server_name localhost; location / { root /usr/share/nginx/html; index index.html index.htm; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://beesSpringboot:8010/; } location /file { alias /home/file; index index.html index.htm; } error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; }}location / :代理的是Angular项目,dist目录内通过Dockerfile COPY在容器内的/usr/share/nginx/html目录;location /file :代理/home/file 目录,作为文件服务器;location /api/ :是为了解决跨域而做的反向代理,为了脱离宿主机的限制,接口所在容器的ip通过别名beesSpringboot来代替。别名的设置是在docker-compose.yml中设置的,后续再讲。Dockerfile构建镜像同样创建一个目录,包含Angualr的dist目录、Dockerfile和nginx的default.conf文件,目录结构如下:[root@Kerry angular]# tree.├── dist│ └── Bees│ ├── 0.cb202cb30edaa3c93602.js│ ├── 1.3ac3c111a5945a7fdac6.js│ ├── 2.99bfc194c4daea8390b3.js│ ├── 3.50547336e0234937eb51.js│ ├── 3rdpartylicenses.txt│ ├── 4.53141e3db614f9aa6fe0.js│ ├── assets│ │ └── images│ │ ├── login_background.jpg│ │ └── logo.png│ ├── favicon.ico│ ├── index.html│ ├── login_background.7eaf4f9ce82855adb045.jpg│ ├── main.894e80999bf907c5627b.js│ ├── polyfills.6960d5ea49e64403a1af.js│ ├── runtime.37fed2633286b6e47576.js│ └── styles.9e4729a9c6b60618a6c6.css├── Dockerfile└── nginx └── default.confDockerfile文件如下:FROM nginxCOPY nginx/default.conf /etc/nginx/conf.d/RUN rm -rf /usr/share/nginx/html/*COPY /dist/Bees /usr/share/nginx/htmlCMD [“nginx”, “-g”, “daemon off;"]以上,通过下列命令,构建bees-nginx-angular的镜像完成:docker build -t bees-nginx-angular . docker-compose容器服务编排上述,我们已经构建了三个镜像,相对应的至少要启动三个容器来完成项目的运行。那要执行三个docker run?太麻烦了,而且这三个容器之间还需要相互通信,如果只使用docker来做的话,不光启动容器的命令会很长,而且为了容器之间的通信,docker –link 都会十分复杂,这里我们需要一个服务编排。docker的编排名气最大的当然是kubernetes,但我的初衷是让这个项目轻量级,不太希望用户安装偏重量级的kubernetes才能运行,而我暂时又没能解决将三个镜像构建成一个镜像的技术问题,就选择了适中的一个产品–docker-compse。安装docker-compose很简单,这里就不赘言了。安装完之后,随便找个目录,写一个docker-compose.yml文件,然后在该文件所在地方执行一行命令就能将三个容器启动了:#启动docker-compose up -d#关闭docker-compose down这里直接上我写的docker-compose.yml文件version: “2"services: beesMysql: restart: always image: bees-mysql ports: - 3306:3306 volumes: - /bees/docker_volume/mysql/conf:/etc/mysql/conf.d - /bees/docker_volume/mysql/logs:/logs - /bees/docker_volume/mysql/data:/var/lib/mysql environment: MYSQL_ROOT_PASSWORD: kerry beesSpringboot: restart: always image: bees-springboot ports: - 8010:8010 depends_on: - beesMysql beesNginxAngular: restart: always image: bees-nginx-angular ports: - 8000:80 depends_on: - beesSpringboot volumes: - /bees/docker_volume/nginx/nginx.conf:/etc/nginx/nginx.conf - /bees/docker_volume/nginx/conf.d:/etc/nginx/conf.d - /bees/docker_volume/nginx/file:/home/fileimage:镜像名称ports:容器的端口和宿主机的端口的映射services:文中三个service,在各自容器启动后就会自动生成别名,例如:在springboot中访问数据库,只需要通过“beesMysql:3306”就能访问。depends_on:会设置被依赖的容器启动之后,才会启动自己。例如:mysql数据库容器启动后,再启动springboot接口的容器。volumes:挂载卷,一些需要长久保存的文件,可通过宿主机中的目录,挂载到容器中,否则容器重启后会丢失。例如:数据库的数据文件;nginx的配置文件和文件服务器目录。其他自动部署为了提高开发效率,简单写了一个自动部署的脚本,直接贴脚本了:#!/bin/bashv_springboot_jar=find /bees/devops/upload/ -name "*.jar"echo “找到jar:"$v_springboot_jarv_angular_zip=find /bees/devops/upload/ -name "dist.zip"echo “找到dist:"$v_angular_zipcd /bees/conf/docker-compose downecho “关闭容器"docker rmi -f $(docker images | grep “bees-springboot” | awk ‘{print $1}’)docker rmi -f $(docker images | grep “bees-nginx-angular” | awk ‘{print $1}’)echo “删除镜像"cd /bees/devops/dockerfiles/springboot/rm -f *.jarcp $v_springboot_jar ./bees-0.0.1-SNAPSHOT.jardocker build -t bees-springboot .echo “生成springboot镜像"cd /bees/devops/dockerfiles/angular/rm -rf dist/cp $v_angular_zip ./dist.zipunzip dist.ziprm -f dist.zipdocker build -t bees-nginx-angular .echo “生成angular镜像"cd /bees/conf/docker-compose up -decho “启动容器"docker ps遇到的坑一开始在docker-compose.yml文件中写services时,每个service不是驼峰式命名,而是下划线连接,例如:bees_springboot、bees_mysql、bees_nginx_angular 。在springboot中访问数据库的别名可以,但是在nginx中,反向代理springboot接口地址时死活代理不了 bees_springboot的别名。能在bees_nginx_angular的容器中ping通bees_springboot,但是代理不了bees_springboot地址的接口,通过curl -v 查看原因,是丢失了host。最后发现,nginx默认request的header中包含“_”下划线时,会自动忽略掉。我因此把docker-compose.yml中service名称,从下划线命名都改成了驼峰式。当然也可以通过在nginx里的nginx.conf配置文件中的http部分中添加如下配置解决:underscores_in_headers on; ...

January 6, 2019 · 2 min · jiezi

Apache 工作的三种模式:Prefork、Worker、Event

Apache 的三种工作模式(Prefork、Worker、Event)Web服务器Apache目前一共有三种稳定的MPM(Multi-Processing Module,多进程处理模块)模式。它们分别是prefork,worker、event,它们同时也代表这Apache的演变和发展。本文原文转自米扑博客:Apache 工作的三种模式:Prefork、Worker、Event如何查看我们的Apache的工作模式呢?可以使用httpd -V 命令查看,如我安装的Apache 2.4版本。# httpd -VServer version: Apache/2.4.34 (Unix)Server built: Aug 2 2018 19:44:29Server’s Module Magic Number: 20120211:79Server loaded: APR 1.6.3, APR-UTIL 1.6.1Compiled using: APR 1.6.3, APR-UTIL 1.6.1Architecture: 64-bitServer MPM: event threaded: yes (fixed thread count) forked: yes (variable process count)或者,更直接的命令 httpd -l 或 apachectl -V | grep -i mpm# httpd -lCompiled in modules: core.c mod_so.c http_core.c event.c# apachectl -V | grep -i mpmServer MPM: event这里使用的是event模式,在apache的早期版本2.0默认prefork,2.2版本是worker,2.4版本是event,详见米扑博客:Apache 服务器负载低访问慢的原因分析和优化方案在configure配置编译参数的时候,可以使用–with-mpm=prefork|worker|event 来指定编译为那一种MPM,当然也可以用编译为三种都支持:–enable-mpms-shared=all,这样在编译的时候会在modules目录下自动编译出三个MPM文件的so,然后通过修改httpd.conf配置文件更改MPM1、Prefork MPM Prefork MPM实现了一个非线程的、预派生的web服务器。它在Apache启动之初,就先预派生一些子进程,然后等待连接;可以减少频繁创建和销毁进程的开销,每个子进程只有一个线程,在一个时间点内,只能处理一个请求。这是一个成熟稳定,可以兼容新老模块,也不需要担心线程安全问题,但是一个进程相对占用资源,消耗大量内存,不擅长处理高并发的场景。图片描述如何配置在Apache的配置文件httpd.conf的配置方式:<IfModule mpm_prefork_module> StartServers 5 MinSpareServers 5 MaxSpareServers 10 MaxRequestWorkers 250 MaxConnectionsPerChild 1000 </IfModule> StartServers 服务器启动时建立的子进程数量,prefork默认是5,MinSpareServers 空闲子进程的最小数量,默认5;如果当前空闲子进程数少于MinSpareServers ,那么Apache将以最大每秒一个的速度产生新的子进程。此参数不要设的太大。MaxSpareServers 空闲子进程的最大数量,默认10;如果当前有超过MaxSpareServers数量的空闲子进程,那么父进程会杀死多余的子进程。次参数也不需要设置太大,如果你将其设置比MinSpareServers 小,Apache会自动将其修改为MinSpareServers +1的数量。MaxRequestWorkers 限定服务器同一时间内客户端最大接入的请求数量,默认是256;任何超过了MaxRequestWorkers限制的请求都要进入等待队列,一旦一个个连接被释放,队列中的请求才将得到服务,如果要增大这个数值,必须先增大ServerLimit。在Apache2.3.1版本之前这参数MaxRequestWorkers被称为MaxClients。MaxConnectionsPerChild 每个子进程在其生命周期内允许最大的请求数量,如果请求总数已经达到这个数值,子进程将会结束,如果设置为0,子进程将永远不会结束。在Apache2.3.9之前称之为MaxRequestsPerChild。这里建议设置为非零,注意原因: 1)能够防止(偶然的)内存泄漏无限进行,从而耗尽内存。 2)给进程一个有限寿命,从而有助于当服务器负载减轻的时候减少活动进程的数量(重生的机会)。2、Worker MPM 和prefork模式相比,worker使用了多进程和多线程的混合模式,worker模式也同样会先预派生一些子进程,然后每个子进程创建一些线程,同时包括一个监听线程,每个请求过来会被分配到一个线程来服务。线程比起进程会更轻量,因为线程是通过共享父进程的内存空间,因此,内存的占用会减少一些,在高并发的场景下会比prefork有更多可用的线程,表现会更优秀一些;另外,如果一个线程出现了问题也会导致同一进程下的线程出现问题,如果是多个线程出现问题,也只是影响Apache的一部分,而不是全部。由于用到多进程多线程,需要考虑到线程的安全了,在使用keep-alive长连接的时候,某个线程会一直被占用,即使中间没有请求,需要等待到超时才会被释放(该问题在prefork模式下也存在)。图片描述如何配置在Apache的配置文件httpd.conf的配置方式:<IfModule mpm_worker_module> StartServers 3 ServerLimit 16 MinSpareThreads 75 MaxSpareThreads 250 ThreadsPerChild 25 MaxRequestWorkers 400 MaxConnectionsPerChild 1000 </IfModule> 配置参数解释:StartServers 服务器启动时建立的子进程数量,在workers模式下默认是3.ServerLimit 系统配置的最大进程数量,默认不显示,自己添加上MinSpareThreads 空闲子进程的最小数量,默认75MaxSpareThreads 空闲子进程的最大数量,默认250ThreadsPerChild 每个子进程产生的线程数量,默认是64MaxRequestWorkers / MaxClients 限定服务器同一时间内客户端最大接入的请求数量.MaxConnectionsPerChild 每个子进程在其生命周期内允许最大的请求数量,如果请求总数已经达到这个数值,子进程将会结束,如果设置为0,子进程将永远不会结束。在Apache2.3.9之前称之为MaxRequestsPerChild。这里建议设置为非零,注意原因:1)能够防止(偶然的)内存泄漏无限进行,从而耗尽内存; 2)给进程一个有限寿命,从而有助于当服务器负载减轻的时候减少活动进程的数量(重生的机会)。Worker模式下所能同时处理的请求总数是由子进程总数乘以ThreadsPerChild值决定的,应该大于等于MaxRequestWorkers。如果负载很大,现有的子进程数不能满足时,控制进程会派生新的子进程。默认ServerLimit 最大的子进程总数是16,加大时也需要显式声明ServerLimit(最大值是20000)。需要注意的是,如果显式声明了ServerLimit,那么它乘以 MaxRequestWorkers必须是hreadsPerChild的整数倍,否则 Apache将会自动调节到一个相应值。3、Event MPM 这是Apache最新的工作模式,它和worker模式很像,不同的是在于它解决了keep-alive长连接的时候占用线程资源被浪费的问题,在event工作模式中,会有一些专门的线程用来管理这些keep-alive类型的线程,当有真实请求过来的时候,将请求传递给服务器的线程,执行完毕后,又允许它释放。这增强了在高并发场景下的请求处理。图片描述如何配置在Apache的配置文件httpd.conf的配置方式:<IfModule mpm_event_module> StartServers 3 ServerLimit 16 MinSpareThreads 75 MaxSpareThreads 250 ThreadsPerChild 25 MaxRequestWorkers 400 MaxConnectionsPerChild 1000 </IfModule> event 模式与 worker 模式完全一样,参考 worker 模式参数即可,这里不再重复。Apache httpd 能更好的为有特殊要求的站点定制。例如,要求更高伸缩性的站点可以选择使用线程的 MPM,即 worker 或 event; 需要可靠性或者与旧软件兼容的站点可以使用 prefork。常见问题查看apache的error日志,可以发现许多系统运行中的问题。server reached MaxRequestWorkers setting[mpm_prefork:error] [pid 1134] AH00161: server reached MaxRequestWorkers setting, consider raising the MaxRequestWorkers setting进程或者线程数目达到了MaxRequestWorkers,可以考虑增加这个值,当然先考虑增加硬件,如内存大小、CPU、SSD硬盘等。scoreboard is full[mpm_event:error] [pid 7555:tid 140058436118400] AH00485: scoreboard is full, not at MaxRequestWorkers这个问题好像是apache2自带的bug,我们无力解决。好在这个问题一般只会影响单个线程,所以暂时可以忍。StackOverflow: Scoreboard is full,not at MaxRequestWorkers 1、I had this same problem. I tried different Apache versions and MPMs. I seem to get this alot with MPM Worker. Also error does not reoccur using Apache 2.2.2,Are you using cPanel? IF so try /upcp –force and increase StartServers to a higher amount like 50 as that’s all I did to get this error away.2、Try EnableMMAP Off in 00_default_settings.confapache 主要版本有:Version 2.4 (Current)Version 2.2 (Historical)Version 2.0 (Historical)Version 1.3 (Historical)参考:https://httpd.apache.org/docs/ 关于 Apache 配置优化,请参见米扑博客:Apache 服务器负载低访问慢的原因分析和优化方案 ...

January 6, 2019 · 2 min · jiezi

nginx docker容器配置https(ssl)

证书生成首先需要有https的证书文件,如果你已经向证书授权中心购买了证书,可以跳过这步,这里介绍如何生成自签名证书,自签名证书是指不是证书授权中心(Certificate Authority)颁发的证书,而是在个人计算机上通过相关工具自己生成的证书,一般用于测试,不可用于生产环境。为了方便管理证书(证书生成过程中会产生很多文件),我们可以单独创建一个目录用于存放证书文件,下面是通过openssl工具生成证书的过程。1. 创建目录$ cd ~$ mkdir ssl$ cd ssl2. 创建秘钥文件创建秘钥文件definesys.key,名称可以自定义,需要指定密码(随意密码即可)$ openssl genrsa -des3 -out definesys.key 1024Generating RSA private key, 1024 bit long modulus…….++++++………………++++++e is 65537 (0x10001)Enter pass phrase for definesys.key:Verifying - Enter pass phrase for definesys.key:3. 创建csr证书需要输入相关信息,比较重要的是Common Name,这个是访问nginx的地址$ openssl req -new -key definesys.key -out definesys.csrEnter pass phrase for definesys.key:You are about to be asked to enter information that will be incorporatedinto your certificate request.What you are about to enter is what is called a Distinguished Name or a DN.There are quite a few fields but you can leave some blankFor some fields there will be a default value,If you enter ‘.’, the field will be left blank.—–Country Name (2 letter code) [AU]:CNState or Province Name (full name) [Some-State]:ShanghaiLocality Name (eg, city) []:ShanghaiOrganization Name (eg, company) [Internet Widgits Pty Ltd]:DefinesysOrganizational Unit Name (eg, section) []:DefinesysCommon Name (e.g. server FQDN or YOUR name) []:www.definesys.comEmail Address []:jianfeng.zheng@definesys.comPlease enter the following ’extra’ attributesto be sent with your certificate requestA challenge password []:可以不用输An optional company name []:可以不用输#此时文件$ ssl lltotal 16-rw-r–r– 1 asan staff 733 1 3 23:57 definesys.csr-rw-r–r– 1 asan staff 963 1 3 23:55 definesys.key4. 去除秘钥密码nginx使用私钥时需要去除密码,执行以下命令时需要输入秘钥的密码$ cp definesys.key definesys.key.bak$ openssl rsa -in definesys.key.bak -out definesys.keyEnter pass phrase for definesys.key.bak:writing RSA key5. 生成crt证书$ openssl x509 -req -days 3650 -in definesys.csr -signkey definesys.key -out definesys.crtSignature oksubject=/C=CN/ST=Shanghai/L=Shanghai/O=Definesys/OU=Definesys/CN=www.definesys.com/emailAddress=jianfeng.zheng@definesys.comGetting Private key#此时文件列表$ ssl lltotal 32-rw-r–r– 1 asan staff 1017 1 4 00:03 definesys.crt-rw-r–r– 1 asan staff 733 1 3 23:57 definesys.csr-rw-r–r– 1 asan staff 887 1 4 00:02 definesys.key-rw-r–r– 1 asan staff 963 1 4 00:01 definesys.key.baknginx容器配置1. 证书文件上传将definesys.crt文件和definesys.key文件拷贝到服务器上,假设你服务器上nginx的配置文件在/etc/nginx/目录下,可以在该目录下创建一个文件夹,这里命名certs,将文件拷贝至该文件夹下。2. 配置文件修改修改配置文件nginx.confserver { listen 443 ssl; server_name www.definesys.com; ssl_certificate /etc/nginx/certs/definesys.crt; ssl_certificate_key /etc/nginx/certs/definesys.key; ssl_session_cache shared:SSL:1m; ssl_session_timeout 5m; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; location / { root /usr/share/nginx/html; index index.html index.htm; }}如果server配置不在nginx.conf文件上,可以在conf.d文件夹下找.conf后缀的文件,一般有个default.conf文件。3. 启动容器docker run -d –restart=unless-stopped -p 443:443 -v /etc/nginx/:/etc/nginx -v /var/run/docker.sock:/tmp/docker.sock:ro -v /u01/application:/usr/share/nginx/html nginx访问https://localhost验证配置是否正确,如果能够正常访问说明配置成功,由于是自签名证书,打开时会提示证书不安全,忽略即可。 ...

January 4, 2019 · 2 min · jiezi

NGINX源码阅读

NGINX源码阅读前言源码版本:2018-10-02 nginx-1.15.5本文主要描述Darwin环境下的流程,与Linux环境下类似,Win32环境下可能会减少部分流程Darwin/Linux等nix类系统使用多进程方式运行,而Win32使用多线程方式运行ngx_ 开头的变量多为全局变量ngx_model_name.c 多为处理nginx配置中相应模块的配置处理,ngx_model_name_core_module.c 多为该模块的核心(通用)处理逻辑nginx架构进程工作模式多进程master进程用于接收外部信号发送给worker进程,如stop、restart、reload等监控worker进程运行状态,worker异常退出后重新启动新的worker进程缓存管理worker进程处理基本网络事件,如http、mail请求等单进程调试情况下使用,直接使用单进程处理网络事件配置daemon配置块:main值类型:flag默认值:1可选范围:on、off说明:是否使用守护进程模式开启服务master_process配置块:main值类型:flag默认值:1可选范围:on、off说明:是否开启master管理进程,主要用于nginx开发调试,关闭状态下不会开启accept互斥锁timer_resolution配置块:main值类型:time默认值:0可选范围:y、M、w、d、h、m、s、ms,解析出的毫秒值 1-[32位:2147483647;64位:9223372036854775807;]说明:用于控制gettimeofday()的系统调用时机,未设置的情况下每次系统事件都将调用gettimeofday(),设置后将以该时间调用gettimeofday()pid配置块:main值类型:string默认值:logs/nginx.pid(根据编译时设置,未设置则为该值)可选范围:—说明:用于设置主进程pid的存放路径lock_file配置块:main值类型:string默认值:logs/nginx.lock(根据编译时设置,未设置则为该值)可选范围:—说明:用于不支持原子操作的系统使用文件锁作为accept互斥锁,支持原子操作的系统将忽略该值worker_processes配置块:main值类型:unit|string(auto)默认值:1可选范围:1-[32位:2147483647;64位:9223372036854775807;]说明:设置worker进程数,建议使用auto由程序根据当前系统环境CPU核数设置(等于CPU核数,如果小于1则设置为1)debug_points配置块:main值类型:uint默认值:0可选范围:1-[32位:2147483647;64位:9223372036854775807;]说明:用于监测到内部错误时中止或停止进程以进行进一步的调试user配置块:main值类型:string默认值:nobody [nobody]可选范围:-说明:设置worker进程使用什么用户和用户组身份启动worker_priority配置块:main值类型:int默认值:0可选范围:-20-20说明:设置worker进程的调度优先级,数值越小表示优先级越高worker_cpu_affinity配置块:main值类型:umask|string(auto)默认值:—可选范围:—说明:将工作进程绑定到CPU组用以减少CPU上下文切换,默认情况下不绑定,设置不存在的CPU掩码将被忽略worker_rlimit_nofile配置块:main值类型:uint默认值:—可选范围:—说明:修改worker进程的最大文件描述符限制,用于在不重新启动master进程的情况下修改限制worker_rlimit_core配置块:main值类型:uint默认值:—可选范围:—说明:修改worker进程的核心文件最大限制,用于在不重新启动master进程的情况下修改限制worker_shutdown_timeout配置块:main值类型:time默认值:0可选范围:y、M、w、d、h、m、s、ms,解析出的毫秒值 1-[32位:2147483647;64位:9223372036854775807;]说明:设置worker进程的结束等待时间(收到结束信号后worker可能还有未处理完的请求,默认系统将等待所有请求处理完成后退出),设置该时间后worker将在到期时直接关闭所有持有的连接working_directory配置块:main值类型:string默认值:—可选范围:—说明:设置工作目录env配置块:main值类型:string默认值:TZ可选范围:—说明:默认情况下程序在启动时将移除除了时区外的所有环境变量,可用该参数设置需要的环境变量load_module配置块:main值类型:string默认值:—可选范围:—说明:用于加载动态模块启动阶段master处理流程日志初始化初始化日志格式打开日志文件获取文件句柄(读写删共享锁)SSL初始化保存启动参数解析配置文件路径运行环境系统初始化获取cpu缓存块大小(如果有一级CPU缓存则使用一级缓存大小)ngx_cacheline_size获取内存页大小 ngx_pagesize获取CPU核数(如果小于一核则设置为1核)压缩表初始化如果为继承关系则继承原socket信息模块预初始化(将所需加载的模块信息加入全局变量,程序所需加载的模块列表在程序编译时根据配置输出至ngx_modules全局变量中)轮训初始化加载配置加载模块创建共享内存关闭无用socket和已打开文件socket监听及配置如果是检查配置文件错误则返回结果如果有接收到nginx信号则转发外部信号给worker进程(通过跨进程事件OpenEvent/SetEvent)注册信号处理器守护进程模式下fork出子进程(当前进程结束),重定向子进程输入、输出至/dev/null(通过tup2)创建pid文件启动worker进程循环设置信号处理器(阻塞部分系统信号,接收到系统信号后转由信号处理函数处理,避免master在处理事件过程中被系统直接kill。在循环管理状态中通过sigsuspend进行阻塞等待信号,子进程退出会接收到CHLD信号)启动worker进程检查是否有模块使用缓存服务如有则启动缓存管理进程进入循环管理状态(处理nginx和系统信号,监控worker进程)worker处理流程初始化设置进程执行优先级设置进程可打开的最大文件描述符如果以root身份启动进程则根据nginx.conf配置设置进程运行的用户、用户组如果有配置尝试设置CPU亲缘性(限定进程在某个特定的CPU中调度,减少CPU上下文切换,均衡CPU利用率)解除阻塞系统信号执行各模块init_process设置通道读事件回调循环处理事件及定时器从定时事件红黑树中查找出最近需要执行的定时器时间执行各模块事件初始化检查当前worker是否繁忙(todo),不繁忙则尝试获取事件监听锁(避免惊群效应),如果获取不到则退出本次处理,也可通过配置忽略锁等待I/O事件唤起(如果超时仍未接收到事件则),保存I/O事件相关信息,将读写事件放入队列等待后续处理处理accept事件中接收的post数据释放事件监听锁(允许其他worker继续获取监听锁处理事件)处理时间到达的定时器处理普通事件(非accept事件)中接收的post数据循环处理master通知信号ngx_exiting 检查定时器中是否仍有未处理的事件,没有则退出workerngx_terminate 直接退出workerngx_quit 关闭定时器、关闭socket监听、关闭闲置连接ngx_reopen rotate logs缓存管理处理流程备注:缓存分为manager和loader设置进程类型为helper关闭当前进程的socket监听重置当前进程的最大连接数为512初始化设置进程执行优先级设置进程可打开的最大文件描述符如果以root身份启动进程则根据nginx.conf配置设置进程运行的用户、用户组解除阻塞系统信号执行各模块init_process设置通道读事件回调设置定时器循环处理事件及定时器从定时事件红黑树中查找出最近需要执行的定时器时间执行各模块事件初始化检查当前worker是否繁忙(todo),不繁忙则尝试获取事件监听锁(避免惊群效应),如果获取不到则退出本次处理,也可通过配置忽略锁等待I/O事件唤起(如果超时仍未接收到事件则),保存I/O事件相关信息,将读写事件放入队列等待后续处理处理accept事件中接收的post数据释放事件监听锁(允许其他worker继续获取监听锁处理事件)处理时间到达的定时器(manager目前ngx_http_file_cache_manager仅一个事件,主要用于清理过期的缓存文件;cache目前仅ngx_http_file_cache_loader一个事件,主要用于缓存数据到缓存文件和记录最新访问时间便于LRU)处理普通事件(非accept事件)中接收的post数据事件模块加载流程ngx_init_cyclecreate_confcommand->setinit_confinit_modulengx_event_process_initinit_event(actions.init)init_process(init_thread)add_event事件唤起process_eventsevent_handlengx_worker_process_exitexit_processngx_master_process_exitexit_master主要函数解析事件结构static ngx_event_module_t ngx_kqueue_module_ctx = { &kqueue_name, ngx_kqueue_create_conf, / create configuration / ngx_kqueue_init_conf, / init configuration / { ngx_kqueue_add_event, / add an event / ngx_kqueue_del_event, / delete an event / ngx_kqueue_add_event, / enable an event / ngx_kqueue_del_event, / disable an event / NULL, / add an connection / NULL, / delete an connection /#ifdef EVFILT_USER ngx_kqueue_notify, / trigger a notify /#else NULL, / trigger a notify /#endif ngx_kqueue_process_events, / process the events / ngx_kqueue_init, / init the events / ngx_kqueue_done / done the events / }};模块结构ngx_module_t ngx_event_core_module = { NGX_MODULE_V1, &ngx_event_core_module_ctx, / module context / ngx_event_core_commands, / module directives / NGX_EVENT_MODULE, / module type / NULL, / init master / ngx_event_module_init, / init module / ngx_event_process_init, / init process / NULL, / init thread / NULL, / exit thread / NULL, / exit process / NULL, / exit master / NGX_MODULE_V1_PADDING};ngx_event_module_init检查进程允许打开的最大文件描述符分配进程间共享内存(空间=ngx_accept_mutex+ngx_connection_counter+ngx_temp_number[+ngx_stat_accepted+ngx_stat_handled+ngx_stat_requests+ngx_stat_active+ngx_stat_reading+ngx_stat_writing+ngx_stat_waiting])ngx_event_process_init进程数设置大于1且配置开启竞争锁(默认禁用)则使用 accept 竞争锁ngx_posted_accept_events队列、ngx_posted_events队列、事件定时器、连接池 等资源初始化event_init移除旧cycle事件添加监听事件ngx_event_core_create_conf对配置参数进行初始化ngx_event_core_init_conf判断当前使用的I/O事件(epoll、devpoll、kqueue、select)对部分未设置值的参数默认值ngx_event_accept新增读事件从等待连接的socket队列中获取第一个连接请求(非阻塞)如果连接尚未准备就绪则直接返回如果当前进程可用最大文件描述符超过限制则删除读事件,标记当前进程暂时停止接收新请求重新计算当前进程的忙碌指数(ngx_cycle->connection_n / 8 - ngx_cycle->free_connection_n)将socket信息保存至当前进程连接池中设置I/O事件处理函数监听函数处理(ngx_http_init_connection、ngx_mail_init_connection、ngx_stream_init_connection)ngx_trylock_accept_mutex尝试获取共享accept锁如果锁获取成功则添加accept事件如果获取锁失败且成功添加过accept事件则移除accept事件ngx_event_connect_peer从待连接池中(根据规则)获取一个可用的连接对象创建空白socket从连接池获取一个空闲的连接资源设置socket为非阻塞如果为 stream_proxy 则绑定本地端口建立远端(upstream、proxy等)连接添加读写事件ngx_event_pipe从上游读取数据写入下游为上游添加读事件,如果没有设置过读延迟标识项则添加或删除读超时定时器为下游添加写事件,如果没有设置过写延迟标识项则添加或删除写超时定时器ngx_event_pipe_read_upstream如果上游发生错误、读取完成或读事件尚未就绪则返回如果为预读事件则从预读缓冲区获取数据如果有配置带宽控制策略则计算当前是否超过限制(limit = limit_rate * (now_sec - start_sec + 1) - read_length),如超过则加入定时器延时读取根据条件从各类管道缓冲区中选取合适的读取对上游输入数据进行过滤(input_filter)如果设置有缓存项则将读取的上游数据存储至临时文件中ngx_event_pipe_write_to_downstream对将向下游数据输出及下游输入的数据进行过滤(output_filter)如果循环池内的缓冲区占用大于所设置大小则输出数据缓冲区资源回收如果连续输出超过10次则返回系统繁忙如果所有缓冲区数据均已输出则重置临时文件游标将空闲的缓冲拷贝放回空闲缓冲区中ngx_event_recvmsg如果事件已经到达执行时间则添加到accept事件设置对应的消息控制器从socket获取消息如果尚未有任何消息到达则返回如果消息有截断则循环多次获取从消息中提取其他辅助信息(如socket的头字段、拓展的错误描述等)查找连接是否已经存在udp连接池中,如果存在则执行事件的处理程序,否则将socket信息保存至当前进程udp连接池中再则执行事件的处理程序计算当前进程的忙碌指数ngx_process_events_and_timers(详见:worker处理流程-循环处理事件及定时器)ngx_handle_write_event如果有设置缓冲区最小发送值则添加设置到写socket(写入缓冲区的数据大小超过最小值后才将数据传到协议层)根据事件标识和事件状态添加或移除对应写事件配置eventworker_connections配置块:events值类型:uint默认值:512可选范围:1-[32位:2147483647;64位:9223372036854775807;select事件模型:FD_SETSIZE;同时受限于系统设置的单进程最大文件描述符,如果设置超过将忽略设置值]说明:设置单worker可同时处理的最大连接数(包括接收的请求以及upstream之类对外的请求),已经在运行的nginx减小此值设置小于已经监听的连接数可能引发错误use配置块:events值类型:string默认值:根据当前系统支持的I/O事件通知机制按先后顺序选择(epoll、/dev/poll、kqueue、select)可选范围:epoll、/dev/poll、kqueue、select说明:设置使用特定I/O事件通知机制,不同的事件通知机制可能影响程序运行效率;需在程序编译时加入对应可选参数以支持事件,否则无法设置multi_accept配置块:events值类型:string默认值:0(off)可选范围:on、off说明:设置同一worker是否尽可能多的(在一次accept后继续)接收等待监听队列中的socket,如果程序使用kqueue事件机制强制关闭accept_mutex配置块:events值类型:string默认值:0(off)可选范围:on、off说明:设置是否开启accept互斥锁,用于控制是否多个worker同时accept(避免惊群效应但是会降低一定效率,由于worker数一般与CPU核数一致进程数少影响较小故默认关闭),如果worker数小于等于1或Win32环境下强制关闭accept_mutex_delay配置块:events值类型:uint默认值:500(毫秒)可选范围:1-[32位:2147483647;64位:9223372036854775807;]说明:设置accept互斥锁延迟时间,当某个worker超过文件最大描述符且未启用互斥锁时下一次尝试accept事件的等待时间,以及I/O事件的最大等待时间debug_connection配置块:events值类型:string默认值:无可选范围:说明:设置开启debug日志的连接,需编译时开启debug参数方可设置生效devpolldevpoll_changesdevpoll_eventsepollepoll_eventsworker_aio_requestseventporteventport_eventsiocpiocp_threadspost_acceptexacceptex_readkqueuekqueue_changeskqueue_eventsopensslssl_enginehttp服务初始化流程create_main_confcreate_srv_confcreate_loc_confpreconfigurationinit_main_confmerge_srv_confmerge_loc_confngx_http_init_static_location_treesngx_http_init_phasespostconfigurationngx_http_optimize_servers优化server信息列表添加各server所需地址的监听设置监听参数(如:backlog、reuseport等),并将监听处理器设置为ngx_http_init_connection建立连接流程查找请求对应的server配置设置当前server的读控制器(根据配置可能为:ssl、http2、http)如果读事件尚未准备就绪则将连接放回队列并添加读事件如果读事件(数据)已经准备就绪则执行对应读控制器(对应https、http2、http)https尝试从socket中读取数据,没有则添加读事件后返回创建ssl连接并和客户端完成握手,若尚未完成则返回等待下次处理如果客户端使用的是http2协议则进入http2处理流程,否则进入http处理流程http2发送http2的headers frame设置单个stream的流量控制接收客户端请求数据确认客户端请求数据包含http2连接序言,提取sid等信息并根据请求的frame类型调用对应处理方法HEADERS frame处理检查客户端发送的frame数据是否有错误(包括数据大小是否超过限制、当前处理中的请求是否超过限制等),有错误则发送中止stream推送信号或关闭连接返回错误如果有设置优先级则根据权重及依赖流构建依赖树以确定每个流的发送次序根据stream状态字段格式(压缩或者非压缩)处理状态值如果状态字段未处理完成则继续处理CONTINUATION frame处理header相关信息检查cookie是否有变动,有则缓存cookie进入请求处理流程添加读事件处理输出队列处理已经建立的连接如果输出缓冲区中有数据添加到输出队列中如果请求尚未处理完成则返回继续处理清理临时资源占用,将连接放回连接池中以待复用添加连接闲置超时关闭事件http读取socket中已接收信息,如果信息尚未接收完成则添加读事件后返回从复用连接池中移除当前连接处理header相关信息(host合法性检查、解析uri、header数据是否超过大小限制等)进入请求处理流程添加输出写事件请求处理流程如果为https请求则检查客户端证书是否正确执行各阶段检查器NGX_HTTP_POST_READ_PHASE读取请求内容阶段ngx_http_realip_handler根据配置从请求header的x_real_ip、x_forwarded_for、代理请求来源IP或请求来源IP中获取客户端真实IPNGX_HTTP_SERVER_REWRITE_PHASEserver级别的uri重写阶段ngx_http_rewrite_handler根据rewrite命令(rewrite、return、break、if、set、rewrite_log、uninitialized_variable_warn)执行对应命令的方法(在ngx_http_rewrite_commands中映射)NGX_HTTP_FIND_CONFIG_PHASE寻找location配置阶段,该阶段使用重写之后的uri来查找对应的location,因为location级别可能有重写指令所以可能会被执行多次ngx_http_core_find_config_phase不能添加外部检查器优先静态location、后正则匹配location检查客户端将要发送的body大小是否超过限制,超过则丢弃body数据结束请求NGX_HTTP_REWRITE_PHASElocation级别的uri重写阶段,该阶段执行location基本的重写指令,也可能会被执行多次ngx_http_rewrite_handlerNGX_HTTP_POST_REWRITE_PHASElocation级别重写的最后处理阶段ngx_http_core_post_rewrite_phase不能添加外部检查器用来检查上阶段是否有uri重写,并根据结果跳转到合适的阶段NGX_HTTP_PREACCESS_PHASE访问权限控制的前一阶段,该阶段在权限控制阶段之前,一般用于访问控制ngx_http_degradation_handler当可用内存低于配置阀值时提供降级处理方式(返回204或444 http状态码)ngx_http_limit_conn_handler最大连接数限制ngx_http_limit_req_handler请求频率限制ngx_http_realip_handlerNGX_HTTP_ACCESS_PHASE访问权限控制阶段ngx_http_core_access_phase根据配置综合其他权限控制阶段的结果决定是否拒绝访问ngx_http_access_handler基于IP黑白名单权限控制ngx_http_auth_basic_handler基于Basic的用户密码权限控制ngx_http_auth_request_handler使用外部服务进行权限控制,当外部服务返回2xx时允许访问,当外部服务返回401或403则限制访问NGX_HTTP_POST_ACCESS_PHASE访问权限控制的最后处理阶段ngx_http_core_post_access_phase不能添加外部检查器根据权限控制阶段的执行结果进行相应处理NGX_HTTP_PRECONTENT_PHASE开始内容生成前的阶段ngx_http_mirror_handler将当前接收到的请求镜像一份(创建一份相同的子请求)异步请求第三方服务,可用于流量放大压测、跨环境测试等,第三方服务的返回将被忽略ngx_http_try_files_handler检查指定顺序的文件是否存在,并使用第一个找到的文件进行请求处理,如果未找到任何文件则内部重定向到最后一个参数指定的uriNGX_HTTP_CONTENT_PHASE内容生成阶段,该阶段产生响应,并发送到客户端ngx_http_core_content_phase如果该请求对应location有设置内容控制器(如fastcgi、grpc、proxy等)则使用对应内容控制器生成内容,否则轮询其他内容生成阶段控制器最终如果没有任何内容生成阶段控制器响应该请求则根据访问资源类型,目录访问返回失败(403),文件访问返回失败(404)ngx_http_autoindex_handler处理以 ‘/’ 结尾的GET、HEAD请求,并生成目录列表。 当ngx_http_index_module模块找不到索引文件时,通常会将请求传递给ngx_http_autoindex_module模块ngx_http_dav_handler用于通过WebDAV协议进行文件管理,默认未编译该模块,需使用 –with-http_dav_module 编译启用ngx_http_gzip_static_handler用于发送带有".gz"文件扩展名的预压缩文件,只处理GET、HEAD请求,默认未编译该模块,需使用 –with-http_gzip_static_module 编译启用ngx_http_index_handler处理以 ‘/’ 结尾的GET、POST、HEAD请求,可以触发内部重定向至其他locationngx_http_random_index_handler处理以 ‘/’ 结尾的GET、POST、HEAD请求,并在目录中选择一个随机文件作为索引文件,在ngx_http_index_module模块处理之前,默认未编译该模块,需使用 –with-http_random_index_module 编译启用ngx_http_static_handler用于读取静态文件,只处理GET、POST、HEAD请求NGX_HTTP_LOG_PHASE日志记录阶段ngx_http_log_handler不能添加外部检查器记录访问日志配置httpvariables_hash_max_size配置块:http值类型:uint默认值:1024可选范围:1-[32位:2147483647;64位:9223372036854775807;]说明:设置变量哈希表的最大大小variables_hash_bucket_size配置块:http值类型:uint默认值:64可选范围:1-[32位:2147483647;64位:9223372036854775807;]说明:设置变量哈希表的桶大小server_names_hash_max_size配置块:http值类型:uint默认值:512可选范围:1-[32位:2147483647;64位:9223372036854775807;]说明:设置服务器名称哈希表的最大大小server_names_hash_bucket_size配置块:http值类型:uint默认值:ngx_cacheline_size可选范围:1-[32位:2147483647;64位:9223372036854775807;]说明:设置服务器名称哈希表的存储桶大小server配置块:http值类型:配置块默认值:无可选范围:无说明:设置虚拟服务器的配置connection_pool_size配置块:mainhttp、server值类型:string默认值:64 * 指针长度可选范围:k、K、M、m说明:精确调整每个连接的内存分配request_pool_size配置块:http、server值类型:string默认值:4096可选范围:k、K、M、m说明:精确调整每个请求的内存分配client_header_timeout配置块:http、server值类型:time默认值:60000可选范围:y、M、w、d、h、m、s、ms,解析出的毫秒值 1-[32位:2147483647;64位:9223372036854775807;]说明:设置读取客户端请求头信息的超时时间client_header_buffer_size配置块:http、server值类型:string默认值:1024可选范围:k、K、M、m说明:设置缓冲区大小以读取客户端请求头信息,当请求头大于此值时将尝试使用large_client_header_buffers用以缓冲信息(但large_client_header_buffers的可用数量一般较小)large_client_header_buffers配置块:http、server值类型:uint size默认值:4 8192可选范围:1-[32位:2147483647;64位:9223372036854775807;];k、K、M、m说明:设置用于读取客户端大请求头信息缓冲区的最大数量和大小ignore_invalid_headers配置块:http、server值类型:flag默认值:1可选范围:on、off说明:控制是否应忽略具有无效名称的请求头字段(有效名称由英文字母、数字、连字符、下划线组成)merge_slashes配置块:http、server值类型:flag默认值:1可选范围:on、off说明:设置是否将请求URI中重复的 ‘/’ 压缩成单个underscores_in_headers配置块:http、server值类型:flag默认值:0可选范围:on、off说明:设置是否禁用请求头中包含下划线字段的使用location配置块:server值类型:配置块默认值:无可选范围:无说明:根据请求URI单独设置配置(支持正则)listen配置块:http、server值类型:string默认值::80(以超级用户权限运行)、*:8000可选范围:无说明:设置监听请求的地址(IP、端口、Unix域套接字)server_name配置块:http、server值类型:string默认值:““可选范围:无说明:设置虚拟服务器的名称(支持正则)types_hash_max_size配置块:http、server、location值类型:uint默认值:1024可选范围:1-[32位:2147483647;64位:9223372036854775807;]说明:设置类型哈希表的最大大小types_hash_bucket_size配置块:http、server、location值类型:uint默认值:64可选范围:1-[32位:2147483647;64位:9223372036854775807;]说明:设置类型哈希表的存储桶大小types配置块:http、server、location值类型:配置块默认值:NULL可选范围:无说明:将文件扩展名映射到对应的MIME类型default_type配置块:http、server、location值类型:string默认值:text/plain可选范围:types中配置的映射说明:定义默认响应的MIME类型root配置块:http、server、location、location=>if值类型:string默认值:{ 0, NULL }可选范围:无说明:设置请求根目录alias配置块:location值类型:string默认值:无可选范围:无说明:为特定location定义别名路径,如alias /www/www.mudoom.com/,uri为 mudoom.icon,则实际访问 /www/www.mudoom.com/mudoom.icon 资源limit_except配置块:location值类型:配置块默认值:无可选范围:无说明:为特定location增加对某种HTTP请求方法的特殊限制,如限制GET请求只允许某些IP访问client_max_body_size配置块:http、server、location值类型:buf默认值:1 1024 1024可选范围:k、K、M、m、g、G说明:设置客户端请求体的最大大小,Content-Length大于此值的请求将被拒绝,设为0则不限制大小client_body_buffer_size配置块:http、server、location值类型:size默认值:2 * ngx_pagesize可选范围:k、K、M、m、g、G说明:设置客户端请求体的缓冲区大小client_body_timeout配置块:http、server、location值类型:time默认值:60000可选范围:y、M、w、d、h、m、s、ms,解析出的毫秒值 1-[32位:2147483647;64位:9223372036854775807;]说明:设置读取客户端请求头信息的超时时间client_body_temp_path配置块:http、server、location值类型:string默认值:NGX_HTTP_CLIENT_TEMP_PATH(编译时设置)可选范围:无说明:设置用于存储客户端请求信息的临时文件的目录client_body_in_file_only配置块:http、server、location值类型:flag默认值:0可选范围:on、clean、off说明:设置是否将请求体保存至文件中,on为保存且请求结束后不删除临时文件,clean则在请求结束后删除临时文件client_body_in_single_buffer配置块:http、server、location值类型:flag默认值:0可选范围:on、off说明:设置是否将整个客户端请求体保存在单个缓冲区中sendfile配置块:http、server、location、location=>if值类型:flag默认值:0可选范围:on、off说明:设置是否使用sendfile发送文件数据sendfile_max_chunk配置块:http、server、location值类型:size默认值:0可选范围:k、K、M、m说明:设置单个sendfile最大传输量,设置为0表示无限制,无限制的情况下一个快速连接可能导致整个worker进程被占用subrequest_output_buffer_size配置块:http、server、location值类型:size默认值:ngx_pagesize可选范围:k、K、M、m说明:设置用于存储子请求输出缓冲区大小aio配置块:http、server、location值类型:flag|string默认值:0可选范围:on、off、threads[=pool](pool为所使用的线程池名称)说明:设置是否启用异步I/O,仅在FreeBSD和Linux中生效,当在Linux上同时启用AIO和sendfile时,AIO用于大于或等于directio设置中指定大小的文件,而sendfile用于小于该大小的文件或禁用directio被禁用时。默认情况下程序只启用单线程异步I/O,如需启用多线程则在编译时添加参数 –with-threads ,仅在epoll、kqueue、eventport I/O事件类型中有效aio_write配置块:http、server、location值类型:flag默认值:0可选范围:on、off说明:设置是否使用异步I/O写数据,需再aio开启下使用,且仅用于写入从代理服务模块接收的临时文件数据read_ahead配置块:http、server、location值类型:size默认值:0可选范围:k、K、M、m说明:设置读取文件时内核的预读取量directio配置块:http、server、location值类型:flag|size默认值:NGX_OPEN_FILE_DIRECTIO_OFF可选范围:off、k、K、M、m说明:设置使用异步I/O还是sendfile的临界点directio_alignment配置块:http、server、location值类型:size默认值:512可选范围:k、K、M、m说明:设置directio的对齐大小,在Linux下使用XFS时,需要将其增加到4Ktcp_nopush配置块:http、server、location值类型:flag默认值:0可选范围:on、off说明:设置在socket中是否设置(Linux中)TCP_CORK或(FreeBSD中)TCP_NOPUSH参数,仅在sendfile开启有可用。该设置可能会导致数据不会立即发送给客户端,而是等待待发送数据长度超过设定值或手动取消阻塞后发送,可以有助于提高大数据量的发送效率tcp_nodelay配置块:http、server、location值类型:flag默认值:1可选范围:on、off说明:设置在socket中是否设置TCP_NODELAY参数,仅在长连接、无缓冲代理、WebSocket代理中启用。该设置会立即将数据发送给客户端而无需等待到指定长度或时间,可以有助于提高小数据量的发送效率send_timeout配置块:http、server、location值类型:time默认值:60000可选范围:y、M、w、d、h、m、s、ms,解析出的毫秒值 1-[32位:2147483647;64位:9223372036854775807;]说明:设置将响应数据发送给客户端的超时时间,该设置用于两次写操作时间的超时时间而非整个请求响应的时间,客户端在这个时间内如果未响应任何信息则连接将被关闭send_lowat配置块:http、server、location值类型:size默认值:0可选范围:k、K、M、m说明:设置socket最小读写数据量,仅在kqueue I/O事件模型中生效。该设置会使内核在读、写缓冲区中数据量达到该值时才通知进程可读写,可以有助于内核唤起进程次数提高CPU利用率postpone_output配置块:http、server、location值类型:size默认值:1460可选范围:k、K、M、m说明:设置向客户端传输响应数据的最小值,当请求未结束、程序未强制执行刷写指令且待传输数据大于该值时才开始向客户端传输响应数据limit_rate配置块:http、server、location值类型:size默认值:0可选范围:k、K、M、m说明:设置向客户端每个连接传输响应数据的速率(bytes/second),为0时表示无限制limit_rate_after配置块:http、server、location值类型:size默认值:0可选范围:k、K、M、m说明:设置初始无响应速率限制阀值,超过该值后开始按limit_rate限制keepalive_timeout配置块:http、server、location值类型:time [time]默认值:75000可选范围:y、M、w、d、h、m、s、ms,解析出的毫秒值 1-[32位:2147483647;64位:9223372036854775807;]说明:第一个值用于控制程序长连接超时时间,第二个值用于向客户端响应的header中添加长连接超时时间,部分浏览器会根据该header控制长连接keepalive_requests配置块:http、server、location值类型:uint默认值:100可选范围:1-[32位:2147483647;64位:9223372036854775807;]说明:设置一个长连接的最大请求数,当请求数超过该值则关闭该长连接keepalive_disable配置块:http、server、location值类型:string默认值:NGX_CONF_BITMASK_SET | NGX_HTTP_KEEPALIVE_DISABLE_MSIE6可选范围:user_agent中配置的映射说明:设置在某些浏览器下禁用长连接satisfy配置块:http、server、location值类型:flag默认值:0可选范围:all、any说明:设置访问权限控制中是满足任意模块还是满足所有模块才允许访问internal配置块:location值类型:无默认值:0可选范围:无说明:设置特定location只允许内部(重定向)请求,其他请求均返回404,且每个请求最多允许10次(NGX_HTTP_MAX_URI_CHANGES)内部重定向lingering_close配置块:http、server、location值类型:flag默认值:1可选范围:on、off、always说明:控制程序如何关闭客户端连接,on为如果客户端正在发送数据则等待数据接收完成后关闭连接,always为无条件等待指定时间(受lingering_time、lingering_timeout综合影响)后关闭连接,off为立即关闭连接lingering_time配置块:http、server、location值类型:time默认值:30000可选范围:y、M、w、d、h、m、s、ms,解析出的毫秒值 1-[32位:2147483647;64位:9223372036854775807;]说明:设置延迟关闭客户端连接等待时间lingering_timeout配置块:http、server、location值类型:time默认值:5000可选范围:y、M、w、d、h、m、s、ms,解析出的毫秒值 1-[32位:2147483647;64位:9223372036854775807;]说明:设置延迟关闭连接每次接收数据的超时时间(每次超时时间内接收到数据都将进入下一次超时计算,但最长不会超过lingering_time)reset_timedout_connection配置块:http、server、location值类型:flag默认值:0可选范围:on、off说明:设置是否重置连接超时,在socket关闭后tcp将向客户端RST以引导客户端重新建立连接absolute_redirect配置块:http、server、location值类型:flag默认值:1可选范围:on、off说明:设置是否为绝对(路径)重定向,on为绝对(路径)重定向,off为相对(路径)重定向server_name_in_redirect配置块:http、server、location值类型:flag默认值:0可选范围:on、off说明:设置是否根据客户端请求的host返回重定向,on为使用实际请求host重定向,off为使用客户端请求host重定向port_in_redirect配置块:http、server、location值类型:flag默认值:1可选范围:on、off说明:设置是否在绝对(路径)重定向中返回端口msie_padding配置块:http、server、location值类型:flag默认值:1可选范围:on、off说明:设置是否向IE客户端请求http状态码大于400的响应中添加空白填充以达到512字节msie_refresh配置块:http、server、location值类型:flag默认值:0可选范围:on、off说明:设置是否向IE客户端请求发送refreshes(而非重定向)命令log_not_found配置块:http、server、location值类型:flag默认值:1可选范围:on、off说明:设置收否记录文件不存在错误日志log_subrequest配置块:http、server、location值类型:flag默认值:0可选范围:on、off说明:设置收否记录子请求日志recursive_error_pages配置块:http、server、location值类型:flag默认值:0可选范围:on、off说明:设置是否使用error_page重定向请求server_tokens配置块:http、server、location值类型:flag|string默认值:1可选范围:off、on、build说明:设置是否在请求响应header中添加nginx版本号等信息if_modified_since配置块:http、server、location值类型:flag默认值:1可选范围:off、exact、before说明:设置如何根据请求header中的If-Modified-Since响应请求,off为忽略请求头,exact为精确匹配,before为小于或等于max_ranges配置块:http、server、location值类型:uint默认值:0x7fffffff可选范围:1-[32位:2147483647;64位:9223372036854775807;]说明:设置请求header中Ranges允许接收的最大值chunked_transfer_encoding配置块:http、server、location值类型:flag默认值:1可选范围:off、on说明:设置禁用 HTTP/1.1 的数据分片传输etag配置块:http、server、location值类型:flag默认值:1可选范围:off、on说明:设置是否自动在请求响应header中根据返回的静态文件类型添加ETag类型error_page配置块:http、server、location值类型:code uri默认值:无可选范围:无说明:根据http错误码响应指定URI资源(可以为变量)post_action配置块:http、server、location值类型:string默认值:{ 0, NULL }可选范围:无说明:内容生成阶段结束后程序将请求内部重定向至指定的location处理(可用于请求统计之类用途)error_log配置块:http、server、location值类型:file level默认值:logs/error.log error可选范围:无说明:设置日志记录路径open_file_cache配置块:http、server、location值类型:flag默认值:NULL可选范围:off、max、inactive说明:设置文件(读取时)信息缓存,off关闭缓存,max设置最大缓存数,inactive设置缓存过期时间open_file_cache_valid配置块:http、server、location值类型:time默认值:60(秒)可选范围:解析出的毫秒值 1-[32位:2147483647;64位:9223372036854775807;]说明:设置文件信息缓存后再次验证文件信息的时间open_file_cache_min_uses配置块:http、server、location值类型:uint默认值:1可选范围:1-[32位:2147483647;64位:9223372036854775807;]说明:设置文件描述符保存进文件信息缓存的最小访问次数open_file_cache_errors配置块:http、server、location值类型:flag默认值:0可选范围:on、off说明:设置是否保存文件访问错误进文件信息缓存open_file_cache_events配置块:http、server、location值类型:flag默认值:0可选范围:on、off说明:暂未发现使用用途resolver配置块:http、server、location值类型:address … [valid=time] [ipv6=on|off]默认值:创建虚拟解析器可选范围:无说明:配置特定的DNS服务作为解析resolver_timeout配置块:http、server、location值类型:time默认值:30000可选范围:y、M、w、d、h、m、s、ms,解析出的毫秒值 1-[32位:2147483647;64位:9223372036854775807;]说明:设置DNS解析超时时间disable_symlinks配置块:http、server、location值类型:flag默认值:0可选范围:off、on说明:是否禁用符号连接类资源http状态码100NGX_HTTP_CONTINUE目前为止一切正常, 客户端应该继续请求, 如果已完成请求则忽略。程序中定义未使用101NGX_HTTP_SWITCHING_PROTOCOLS服务器应客户端升级协议的请求(Upgrade请求头)正在进行协议切换。程序中proxy、uwsgi、scgi模块将进行协议切换(http2目前不支持该协议协商只支持指定协议请求)102NGX_HTTP_PROCESSING非通用协议,程序中定义未使用200NGX_HTTP_OK请求成功201NGX_HTTP_CREATED请求已经被成功处理,并且创建了新的资源。WebDAV模块中使用,表示(move、overwrite)文件或集合(mkcol)创建成功202NGX_HTTP_ACCEPTED服务器端已经收到请求消息,但是尚未进行处理。程序中定义未使用204NGX_HTTP_NO_CONTENT目前请求成功,但客户端不需要更新其现有页面。206NGX_HTTP_PARTIAL_CONTENT请求已成功,并且主体包含所请求的数据区间,该数据区间是在请求的 Range 首部指定的。300NGX_HTTP_SPECIAL_RESPONSE表示重定向的响应状态码,表示该请求拥有多种可能的响应。用户代理或者用户自身应该从中选择一个。程序中作为特殊响应码的分界值301NGX_HTTP_MOVED_PERMANENTLY被请求的资源已永久移动到新位置,并且将来任何对此资源的引用都应该使用本响应返回的若干个 URI 之一。302NGX_HTTP_MOVED_TEMPORARILY请求的资源现在临时从不同的 URI 响应请求。由于这样的重定向是临时的,客户端应当继续向原有地址发送以后的请求。303NGX_HTTP_SEE_OTHER对应当前请求的响应可以在另一个 URI 上被找到,而且客户端应当采用 GET 的方式访问那个资源。程序定义未使用304NGX_HTTP_NOT_MODIFIED如果客户端发送了一个带条件的 GET 请求且该请求已被允许,而文档的内容(自上次访问以来或者根据请求的条件)并没有改变,则服务器应当返回这个状态码。307NGX_HTTP_TEMPORARY_REDIRECT请求的资源现在临时从不同的URI 响应请求。由于这样的重定向是临时的,客户端应当继续向原有地址发送以后的请求。程序定义未使用308NGX_HTTP_PERMANENT_REDIRECT这意味着资源现在永久位于由 Location: HTTP Response 标头指定的另一个 URI。程序定义未使用400NGX_HTTP_BAD_REQUEST语义有误,当前请求无法被服务器理解。客户端请求信息有误或请求host不存在401NGX_HTTP_UNAUTHORIZED当前请求需要用户验证。该响应必须包含一个适用于被请求资源的 WWW-Authenticate 信息头用以询问用户信息。auth_basic模块权限校验失败或upstream上游返回403NGX_HTTP_FORBIDDEN服务器已经理解请求,但是拒绝执行它。访问权限控制阶段被限制、请求的资源无读取权限、upstream上游返回404NGX_HTTP_NOT_FOUND请求失败,请求所希望得到的资源未被在服务器上发现。405NGX_HTTP_NOT_ALLOWED请求行中指定的请求方法不能被用于请求相应的资源。408NGX_HTTP_REQUEST_TIME_OUT请求超时。接收客户端请求数据超时(客户端指定的Content-Length在指定时间内未能接收完成)409NGX_HTTP_CONFLICT由于和被请求的资源的当前状态之间存在冲突,请求无法完成。WebDAV模块中部分请求操作失败411NGX_HTTP_LENGTH_REQUIRED服务器拒绝在没有定义 Content-Length 头的情况下接受请求。程序定义未使用412NGX_HTTP_PRECONDITION_FAILED服务器在验证在请求的头字段中给出先决条件时,没能满足其中的一个或多个。WebDAV模块中overwrite发生错误或not_modified模块中客户端请求的判断条件没有满足413NGX_HTTP_REQUEST_ENTITY_TOO_LARGE服务器拒绝处理当前请求,因为该请求提交的实体数据大小超过了服务器愿意或者能够处理的范围。414NGX_HTTP_REQUEST_URI_TOO_LARGE请求的URI 长度超过了服务器能够解释的长度,因此服务器拒绝对该请求提供服务。415NGX_HTTP_UNSUPPORTED_MEDIA_TYPE对于当前请求的方法和所请求的资源,请求中提交的实体并不是服务器中所支持的格式,因此请求被拒绝。客户端请求时使用请求体访问WebDAV模块部分方法或image_filter模块中图片资源数据异常416NGX_HTTP_RANGE_NOT_SATISFIABLE如果请求中包含了 Range 请求头,并且 Range 中指定的任何数据范围都与当前资源的可用范围不重合,同时请求中又没有定义 If-Range 请求头,那么服务器就应当返回416状态码。客户端请求header中的Range不合法(格式不正确)421NGX_HTTP_MISDIRECTED_REQUEST该请求针对的是无法产生响应的服务器。 客户端尝试https请求的host于server配置中host不一致429NGX_HTTP_TOO_MANY_REQUESTS用户在给定的时间内请求过于频繁。upstream模块中根据上游服务的返回而返回444NGX_HTTP_CLOSE非通用协议。程序定义未使用494NGX_HTTP_NGINX_CODES非通用协议。程序定义未使用NGX_HTTP_REQUEST_HEADER_TOO_LARGE非通用协议。客户端请求header过大(large_header_buf已被分配完或者请求大于headerlarge_header_buf)495NGX_HTTPS_CERT_ERROR非通用协议。客户端以https协议请求但是程序对客户端证书校验失败496NGX_HTTPS_NO_CERT非通用协议。客户端以https协议请求但是对应的server块未配置证书信息497NGX_HTTP_TO_HTTPS非通用协议。客户端以https协议请求但是对应的server块未开启ssl499NGX_HTTP_CLIENT_CLOSED_REQUEST非通用协议。客户端在程序响应前断开了连接500NGX_HTTP_INTERNAL_SERVER_ERROR服务器遇到了不知道如何处理的情况。程序发生内部错误(如配置错误、重定向错误等)时使用501NGX_HTTP_NOT_IMPLEMENTED此请求方法不被服务器支持且无法被处理。WebDAV模块客户端PUT请求header中使用Range、http客户端请求header中的Transfer-Encoding无法识别时使用502NGX_HTTP_BAD_GATEWAY此错误响应表明服务器作为网关需要得到一个处理这个请求的响应,但是得到一个错误的响应。upstream模块中上游服务响应错误时使用503NGX_HTTP_SERVICE_UNAVAILABLE服务器没有准备好处理请求。 limit_conn(触发请求数限制)、limit_req(触发速率限制)、upstream模块中使用504NGX_HTTP_GATEWAY_TIME_OUT当服务器作为网关,不能及时得到响应时返回此错误代码。upstream模块中上游服务响应超时时使用505NGX_HTTP_VERSION_NOT_SUPPORTED服务器不支持请求中所使用的HTTP协议版本。507NGX_HTTP_INSUFFICIENT_STORAGE服务器有内部配置错误。WebDAV模块中发生NGX_ENOSPC错误时使用mail服务初始化流程create_main_confcreate_srv_confinit_main_confmerge_srv_confngx_mail_add_portsngx_mail_optimize_servers优化监听列表,如果相同端口有泛IP监听则忽略其他指定IP地址的监听添加各server所需地址的监听设置监听参数(如:backlog、rcvbuf等),并将监听处理器设置为ngx_mail_init_connection建立连接流程查找请求对应的server配置(根据server配置可确定当前请求对应的mail协议)设置当前server的读控制器(根据配置可能为:ssl、非ssl)执行对应读控制器(对应ssl、非ssl)ssl创建ssl连接并和客户端完成握手,若尚未完成则添加超时定时器后返回等待下次处理检查客户端证书如果当前server配置中的STARTTLS命令未关闭则执行对应协议(imap、pop3、smtp)的init_protocol方法后返回否则进入session初始化流程非ssl进入session初始化流程session初始化流程imap添加读(ngx_mail_imap_init_protocol)写(ngx_mail_send)事件pop3如果server配置有apop、cram-md5两种身份验证方式则为请求输出加盐(避免暴露破解)添加读(ngx_mail_imap_init_protocol)写(ngx_mail_send)事件smtp添加读(ngx_mail_imap_init_protocol)写(ngx_mail_send)事件协议初始化检查请求是否超时,超时则关闭连接创建临时缓冲区读取客户端请求命令(一次无法读取完则添加读事件返回等待下次处理)如果客户端发送命令数据过大则退出本次请求解析客户端请求命令并根据命令提取相应鉴权验证信息将授权验证信息发送至上游身份验证服务(根据auth_http配置)检查上游身份验证服务的返回结果,如果失败则根据配置绝对是否返回错误信息给客户端,否则将严重服务器身份验证服务返回的邮件服务地址及授权信息(授权密钥)存储于输出缓冲区如果输出缓冲区存在数据则向客户端发送数据stream服务初始化流程create_main_confcreate_srv_confpreconfigurationinit_main_confmerge_srv_confpostconfigurationngx_stream_init_phase_handlers初始化个请求阶段处理控制器ngx_stream_add_portsngx_stream_optimize_servers优化监听列表,如果相同端口有泛IP监听则忽略其他指定IP地址的监听添加各server所需地址的监听设置监听参数(如:backlog、keepalive等)建立连接流程查找请求对应的server配置如果有配置使用accept互斥锁则将读事件添加到普通事件(非accept事件)队列中进入请求处理流程请求处理流程NGX_STREAM_POST_ACCEPT_PHASE读取请求内容阶段ngx_stream_realip_handler获取客户端真实IPNGX_STREAM_PREACCESS_PHASE访问权限控制的前一阶段,该阶段在权限控制阶段之前,一般用于访问控制ngx_stream_limit_conn_handler最大连接数限制NGX_STREAM_ACCESS_PHASE访问权限控制阶段ngx_stream_access_handler基于IP黑白名单权限控制NGX_STREAM_SSL_PHASE访问建立ssl阶段ngx_stream_ssl_handler建立ssl连接NGX_STREAM_PREREAD_PHASE请求数据预读阶段ngx_stream_core_preread_phase创建临时缓冲区读取客户端请求数据如果客户端数据请求尚未接收完成则添加读事件后返回等待下次处理ngx_stream_ssl_preread_handler读取SSL请求数据NGX_STREAM_CONTENT_PHASE内容生成阶段,该阶段产生响应,并发送到客户端ngx_stream_core_content_phase转发客户端请求至对应上游服务,并将上游服务的响应返回给客户端NGX_STREAM_LOG_PHASE日志记录阶段ngx_stream_log_handler不能添加外部检查器记录访问日志参考文档Nginx开发从入门到精通CPU缓存 - 维基百科Page (computer memory))OpenEventLinux管道编程技术:dup函数,dup2函数,open函数详解Nginx Caching红黑树nginx documentationHypertext Transfer Protocol Version 2 (HTTP/2)HTTP 响应代码Configuring NGINX as a Mail Proxy Server源地址 By佐柱转载请注明出处,也欢迎偶尔逛逛我的小站,谢谢 :) ...

December 31, 2018 · 2 min · jiezi

阿里云 centos7.6 安装 nginx 1.14.2

新增nginx 用户 用户组groupadd nginxuseradd -g nginx nginx下载 解压wget “http://nginx.org/download/nginx-1.14.2.tar.gz"tar xzvf nginx-1.14.2.tar.gz#同时下载 清理缓存插件wget “http://labs.frickle.com/files/ngx_cache_purge-2.3.tar.gz"tar xzvf ngx_cache_purge-2.3.tar.gz编译安装需要使用 root用户cd nginx-1.14.2#ngx_cache_purge-2.3 路径是在你刚解压的路径./configure –user=nginx –group=nginx –prefix=/usr/local/nginx –with-http_v2_module –with-http_ssl_module –with-http_sub_module –with-http_flv_module –with-http_stub_status_module –with-http_gzip_static_module –with-pcre –add-module=/home/flame/software/ngx_cache_purge-2.3make && make install加入 systemctlvim /lib/systemd/system/nginx.service输入以下内容[Unit]Description=nginxAfter=network.target [Service]Type=forkingExecStart=/usr/local/nginx/sbin/nginxExecReload=/usr/local/nginx/sbin/nginx -s reloadExecStop=/usr/local/nginx/sbin/nginx -s quitPrivateTmp=true [Install]WantedBy=multi-user.target修改配置文件cd /usr/local/nginx/confmkdir vhostvim nginx.conf输入以下内容#user nobody;worker_processes 1; #设置值和CPU核心数一致error_log /usr/local/nginx/logs/nginx_error.log crit; #日志位置和日志级别pid /usr/local/nginx/nginx.pid;#Specifies the value for maximum file descriptors that can be opened by this process.worker_rlimit_nofile 8192;events{ use epoll; worker_connections 8192;}http{ include mime.types; default_type application/octet-stream; log_format main ‘$remote_addr - $remote_user [$time_local] “$request” ’ ‘$status $body_bytes_sent “$http_referer” ’ ‘"$http_user_agent” $http_x_forwarded_for’; #charset gb2312; server_names_hash_bucket_size 128; client_header_buffer_size 32k; large_client_header_buffers 4 32k; client_max_body_size 8m; sendfile on; tcp_nopush on; keepalive_timeout 60; tcp_nodelay on; fastcgi_connect_timeout 300; fastcgi_send_timeout 300; fastcgi_read_timeout 300; fastcgi_buffer_size 64k; fastcgi_buffers 4 64k; fastcgi_busy_buffers_size 128k; fastcgi_temp_file_write_size 128k; gzip on; gzip_min_length 1k; gzip_buffers 4 16k; gzip_http_version 1.0; gzip_comp_level 2; gzip_types text/plain application/x-javascript text/css application/xml; gzip_vary on; #下面是server虚拟主机的配置 include vhost/.conf;}vim vhost/php.conf输入以下内容server{ listen 8080;#监听端口 server_name xxx.xxx.xxx.xxx;#域名 或ip index index.html index.htm index.php; root /usr/local/nginx/html;#站点目录 location ~ .php$ { root /home/www/webroot/php; fastcgi_pass 127.0.0.1:9080; fastcgi_index index.php; include fastcgi.conf; } location ~ ..(gif|jpg|jpeg|png|bmp|swf|ico)$ { expires 30d; # access_log off; } location ~ .*.(js|css)?$ { expires 15d; # access_log off; } access_log “/usr/local/nginx/logs/php_access.log” main; error_log “/usr/local/nginx/logs/php_error.log” info;}测试启动nginx/usr/local/nginx/sbin/nginx -tsystemctl start nginx#或/usr/local/nginx/sbin/nginxcentos7防火墙开放端口# 开放8080端口firewall-cmd –zone=public –add-port=8080/tcp –permanent#重载firewall-cmd –reload#查看状态firewall-cmd –list-all阿里云端口安全组设置安全组说明新增8080端口的新增【入方向】规则测试打开 xxx.xxx.xxx.xxx:8080 如果有出现 【Welcome to nginx!】 即成功了 ...

December 30, 2018 · 1 min · jiezi

记一次基于react、cra2、typescript的pwa项目由开发到部署(三)

该篇文章为本系列最后一篇文章,因为最近楼主忙于毕设,所以这也是一篇被鸽了很久很久的文章。该文章主要讲的是该项目的部署部分,包括:如何部署将该项目部署到nginx服务器上。为它配置证书,让它运行在https协议上等。项目回顾这是一个基于creat-react-app2的pwa项目。可以添加到主屏幕,可以离线运行。项目地址: browseExpbyReact本篇内容其实完全可以脱离这个项目来看,以下内容对于大多数个人 spa 项目的简单部署都是适用的。如何部署该项目完成后如何部署到服务器呢?本项目使用的web服务器是 nginx。nginx是一个异步的web服务器,使用异步事件驱动来处理请求,是一款面向性能设计的HTTP服务器。首先,为了让我们访问到项目,我们需要给我们的项目配置一个反向代理,将我们对服务器的访问代理到项目;然后,因为我们的项目是一个pwa项目,所以需要给它配置证书,升级为 https,以便让我们可以体验到pwa的特性。先编写一个后端服务首先我们要编写一个后端服务,让我们可以访问到项目的入口页,使用express来简单编写一个服务。const fs = require(‘fs’)const path = require(‘path’)const express = require(’express’)const app = express();app.use(express.static(path.resolve(__dirname, ‘./build’)))app.get(’’, function(req, res) { const html = fs.readFileSync(path.resolve(__dirname, ‘./build/index.html’), ‘utf-8’) res.send(html)})app.listen(3003, function() { console.log(‘server listening on port 3003!’)})通过express,我们在本地的3003端口开启了一个服务,用来访问我们的项目。然后我们需要使用类似于 ftp 等工具将我们的项目上传到我们的服务器,并运行该node服务。那么现在我们的项目就在服务器上的3003端口运行着。配置反向代理我们的项目已经在服务器上的3003端口运行着,所以我们需要配置一个反向代理,将我们对服务器的访问反向代理到服务器的127.0.0.1:3003。在nginx相应的文件夹下添加相关配置文件,通常为nginx文件夹下的conf.d文件夹,本项目在 etc/nginx/conf.d 下添加。在etc/nginx/conf.d 文件新建针对该项目的配置文件holyzheng-top-3002,这里的命名通常有一定的约定,方便自己组织区分项目,我的习惯为二级域名-一级域名-端口。在该文件里添加一下内容:upstream browseexpreact { server 127.0.0.1:3003; # 实例,对应我们的项目}server { listen 80; # http监听的端口 server_name browseexpreact.holyzheng.top; # 我要使用的ip域名 error_page 405 =200 @405; # 允许对静态资源进行POST请求 location @405 { proxy_pass http://browseexpreact; } rewrite ^(.) https://$host$1 permanent; # 将http 重定向到 https}这里的配置的意思是将我们对该服务器的http(默认端口80)请求反向代理到我们的服务器上 127.0.0.1:3003正在运行的实例,也就是我们的项目。升级为 https要升级到https,首先要向我们的服务器商申请证书,然后将证书下载到本地,再上传到自己的服务器上,通常放置于nginx文件夹下的cert文件夹里,本项目为/etc/nginx/cert。证书上传到服务器后,修改我们的配置:upstream browseexpreact { server 127.0.0.1:3003; # 实例}server { listen 80; # http监听的端口 server_name browseexpreact.holyzheng.top; # 我要使用的ip域名 error_page 405 =200 @405; # 允许对静态资源进行POST请求 location @405 { proxy_pass http://browseexpreact; } rewrite ^(.) https://$host$1 permanent; # 将https 重定向到 https}# 增加下面的配置server { listen 443; server_name browseexpreact.holyzheng.top; # 这部分配置在申请证书的时候会有提示,复制粘贴就好 ssl on; ssl_certificate /etc/nginx/cert/cert-1540814527008_browseexpreact.holyzheng.top.crt; ssl_certificate_key /etc/nginx/cert/cert-1540814527008_browseexpreact.holyzheng.top.key; ssl_session_timeout 5m; ssl_protocols SSLv2 SSLv3 TLSv1; ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP; ssl_prefer_server_ciphers on; if ($ssl_protocol = “”) { # 判断用户是否输入协议 rewrite ^(.) https://$host$1 permanent; } location / { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forward-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_set_header X-Nginx-Proxy true; proxy_pass http://browseexpreact; # 要代理的实例 }}关于证书的配置,再申请证书的时候会有提示,将对应的配置复制到自己的配置文件就好。配置中新增了关于https请求(默认断开443)的配置,将我们对服务器的https请求(默认断开443)反向代理到服务器中的127.0.0.1:3003正在运行的实例,就是我们的项目。到目前为止,我们可以通过https请求来访问我们的项目了。让项目持续后台运行目前我们发现,只要我们把服务器端的控制台关闭,那么express服务就会断掉,就无法再访问到这个项目了,所以我们需要一个工具让我们的express服务持续的后台运行。本项目选用的工具为 PM2。PM2是一个带有负载均衡功能的node应用的进程管理器,它能保证进程一直运行着,可以利用它在服务器上同时管理多个node项目。常用基本指令有:npm install pm2 -g : 全局安装。pm2 start app.js : 启动服务,入口文件是app.js。pm2 restart [name or id] : 重启服务。pm2 list : 查看正在运行的项目清单pm2 delete [name or id] :删除项目借助 PM2 我们就可以让我们的项目在服务器上持续运行了。然后我们就可以通过https请求访问我们的项目了。 ...

December 28, 2018 · 1 min · jiezi

Nginx 对访问量的控制

目的了解 Nginx 的 ngx_http_limit_conn_module 和 ngx_http_limit_req_module 模块,对请求访问量进行控制。Nginx 模块化nginx 的内部结构是由核心模块和一系列的功能模块所组成。模块化架构使得每个模块的功能相对简单,实现高内聚,同时也便于对 Nginx 进行功能扩展。针对 web 请求,Nginx 所有开启的模块会组成一条链,类似于闯关游戏中的一道道关卡,每个模块负责特定的功能,例如实现压缩的 ngx_http_gzip_module 模块,实现验证的 ngx_http_auth_basic_module 模块和实现代理的 ngx_http_proxy_module 模块等。连接到服务器的请求,会依次经过Nginx各个模块的处理,只有通过这些模块处理之后的请求才会真正的传递给后台程序代码进行处理。Nginx 并发访问控制对于 web 服务器而言,当遇到网络爬虫,或者恶意大流量攻击访问时,会造成服务器内存和 CPU 爆满,带宽也会跑满,所以作为成熟的服务器代理软件,需要可以对这些情况进行控制。 Nginx 控制并发的方法有两种,一种是通过IP或者其他参数控制其并发量;另外一种是控制单位时间内总的请求处理量。即对并发和并行的控制,这两个功能分别由 ngx_http_limit_conn_module 和 ngx_http_limit_req_module 模块负责实现。ngx_http_limit_conn_module 模块说明该模块主要用于对请求并发量进行控制。参数配置limit_conn_zone指令配置 limit_conn_zone key zone=name:size配置的上下文:http说明:key 是 Nginx 中的变量,通常为 $binary_remote_addr | $server_name;name 为共享内存的名称,size 为该共享内存的大小;此配置会申请一块共享内存空间 name,并且保存 key 的访问情况limit_conn_log_level语法:limit_conn_log_level info|notice|warn|error默认值:error配置上下文:http,server,location说明:当访问达到最大限制之后,会将访问情况记录在日志中limit_conn语法:limit_conn zone_name number配置上下文:http,server,location说明:使用 zone_name 进行访问并发控制,当超过 number 时返回对应的错误码limit_conn_status语法:limit_conn_status code默认值:503配置上下文:http,server,location说明:当访问超过限制 number 时,给客户端返回的错误码,此错误码可以配合 error_page 等参数,在访问超量时给客户返回友好的错误页面limit_rate语法:limit_rate rate默认值:0配置上下文:http,server,location说明:对每个链接的速率进行限制,rate 表示每秒的下载速度;limit_rate_after语法:limit_rate_after size配置上下文:http,server,location说明:此命令和 limit_rate 配合,当流量超过 size 之后,limit_rate 才开始生效简单配置示例limit_conn_zone $binary_remote_addr zone=addr:10m;server { listen 80; server_name www.domain.com; root /path/; index index.html index.htm; location /ip { limit_conn_status 503; # 超限制后返回的状态码; limit_conn_log_level warn; # 日志记录级别 limit_rate 50; # 带宽限制 limit_conn addr 1; # 控制并发访问 } # 当超过并发访问限制时,返回503错误页面 error_page 503 /503.html;}ngx_http_limit_req_module 模块说明该模块主要控制单位时间内的请求数。使用 “leaky bucket” (漏斗)算法进行过滤,在设置好限制 rate 之后,当单位时间内请求数超过 rate 时,模块会检测 burst 值,如果值为0,则请求会依据 delay|nodelay 配置返回错误或者进行等待;如果 burst 大于0时,当请求数大于 rate 但小于 burst 时,请求进入等待队列进行处理。参数配置limit_req_zone语法:limit_req_zone key zone=name:size rate=rate配置上下文:http说明:key 是 Nginx 中的变量,通常为 $binary_remote_addr | $server_name;name 为共享内存的名称,size 为该共享内存的大小;rate 为访问频率,单位为 r/s 、r/m 。此配置会申请一块共享内存空间 name,并且保存 $key 的访问情况;limit_req语法: limit_rate zone=name [burst=number] [nodelay|delay=number]配置上下文:http,server,location说明:开启限制,burst设置最多容量,nodelay决定当请求超量是,是等待处理还是返回错误码;limit_req_log_level 和 limit_req_status 配置参数左右与ngx_http_limit_conn_module模块一致;简单配置示例limit_req_zone $binary_remote_addr zone=req:10m rate=2r/m;server { listen 80; server_name www.domain.com; root /path/; index index.html index.htm; location /limit { limit_req zone=req burst=3 nodelay; } # 当超过并发访问限制时,返回503错误页面 error_page 503 /503.html;}注意这两种访问控制都需要申请内存空间,既然有内存空间,当然会存在内存耗尽的情况,这时新的请求都会被返回错误,所以当开启访问量限制时,需要通过监控防止此类情况发生。小结通过对 Nginx 模块化架构的简单介绍,重点了解 ngx_http_limit_conn_module 和 ngx_http_limit_req_module 模块的功能和配置参数,实现 Nginx 对请求的并发控制。如有不对,还请指教 ...

December 27, 2018 · 1 min · jiezi

网络协议 16 - DNS 协议:网络世界的地址簿

为什么在地址栏输入域名,就能直接访问到对应服务器?全局负载均衡和内部负载均衡又是什么?这些都和 DNS 解析息息相关,让我们一起来解密 DNS 解析。 其实说起 DNS 解析,应该都知道它很像地址簿。就像我们去一家新开的沃尔玛超市,通过地址簿查出来沃尔玛在哪条路多少号,然后再去找。 在网络世界中,也是这样的。我们可以记住网站的名称,但是很难记住网站的 IP 地址,因此需要一个“地址簿”,帮我们将网站名称转换成 IP。这个“地址簿”就是 DNS 服务器。DNS 服务器 对于 DNS 服务器而言,全球每个人上网,都需要访问它。 而全球的网民数,据最新统计,已经有 40 亿,每个人都访问它,可想而知 DNS 服务器会有很大的访问流量压力(高并发)。 而且,它还非常重要,一旦出了故障,整个互联网都将瘫痪(高可用)。 此外,上网的人分布在全世界各地,如果大家都去同一个地方的某一台服务器,时延将会非常的(分布式)。 因此,DNS 服务器一定要具备高可用、高并发、分布式的特点。 基于此,DNS 服务器设计成树状的层次结构。如下图:根 DNS 服务器:返回顶级域 DNS 服务器的 IP 地址;顶级域 DNS 服务器:返回权威 DNS 服务器的 IP 地址;权威 DNS 服务器:返回相应主机的 IP 地址。DNS 解析流程 上面说了 DNS 服务器面临大流量访问的压力,因此,为了提高 DNS 的解析性能,很多网站都会就近部署 DNS 缓存服务器。所以,我们常见的 DNS 解析流程就变成了:客户端发出 DNS 请求给本地域名服务器。我们访问博客园,客户端会问本地域名服务器, www.cnblogs.com 的 IP 是什么?(本地域名服务器,如果网络是通过 DHCP 配置,本地 DNS 是由你的网络服务商,如电信、联通等自动分配,它通常就在网络服务商的机房里);本地 DNS 收到来自客户端的请求,查找“地址簿”,返回 IP 或请求根域名服务器。我们可以理解为服务器上缓存了一张域名与 IP 对应的大表,如果能找到 www.cnblogs.com,就直接返回对应的 IP 地址。如果没有找到,本地 DNS 会去问它的根域名服务器;根 DNS 收到来自本地 DNS 的请求,返回 .com 对应的顶级域名服务器的地址。根域名服务器是最高层次的,全球共有 13 套,它不直接用于域名解析,而是指明怎样去查找对应 IP。它发现请求的域名后缀是 .com,就会返回 .com 对应的顶级域名服务器的地址;本地 DNS 服务器收到顶级 DNS 服务器地址,请求顶级 DNS 服务器查询域名 IP;顶级 DNS 服务器返回权威 DNS 服务器地址。顶级域名服务器就是大名鼎鼎的,负责 .com、.net、.org 这些二级域名,比如 cnblogs.com,它会返回对应的权威 DNS 服务器地址;本地 DNS 服务器收到权威 DNS 服务器地址,请求权威 DNS 服务器查询域名 IP。而 cnblogs.com 的权威 DNS 服务器就是域名解析结果的原出处;权威 DNS 服务器返回对应 IP。权威 DNS 服务器查询“地址簿”,获取到域名对应 IP 地址,返回给本地 DNS 服务器;本地 DNS 服务器收到 IP,返回给客户端;客户端与目标建立连接。 至此,我们完成了 DNS 的解析过程,整个过程如下图:负载均衡 站在客户端角度,上述过程是一次 DNS 递归查询过程。因为本地 DNS 全权为它代劳,它只要坐等结果就好了。在这个过程中,DNS 除了可以通过名称映射为 IP 地址外,它还可以做另外一件很重要的事 - 负载均衡。 还是拿我们逛沃尔玛超市为例。它可能在一个城市里会有多家店,我们要逛沃尔玛,可以就近找一家,而不用都去同一家,这就是负载均衡。DNS 做负载均衡也有花样可以玩。1)DNS 做内部负载均衡 所谓的内部负载均衡,其实很好理解。就像我们的应用访问数据库,在应用里配置的数据库地址。如果配置成 IP 地址,一旦数据库换到了另外一台机器,我们就要修改配置。如果我们有很多台应用同时连一个数据库,一换 IP,就需要将这些应用的配置全部修改一遍,是不是很麻烦?所以,我们可以将数据地址配置成域名。在更换数据库位置时,只要在 DNS 服务器里,将域名映射为新的 IP 地址就可以了。 在这个基础上,我们可以更进一步 。例如,某个应用要访问另外一个应用,如果配置另外一个应用的 IP 地址,那么这个访问就是一对一的。但是当被访问的应用因流量过大撑不住的时候,我们就需要部署多个应用。这时候,我们就不能直接配置成 IP,而是要配置域名了。只要在域名解析的时候,配置好策略,这次返回一个 IP,下次返回第二个 IP,就实现了负载均衡。2)DNS 做全局负载均衡 为了保证我们应用的高可用性,往往会将应用部署在多个机房,每个地方都会有自己的 IP 地址。当用户访问某个域名的时候,这个 IP 地址可以轮询访问多个数据中心。如果一个数据中心因为某种原因挂了,只要将这个 IP 地址从 DNS 服务器中删掉就可以了,用户不会访问到宕机的服务器,保证了应用的可用性。 另外,我们肯定希望用户能访问就近的数据中心。这样客户访问速度就会快很多,体验也会好很多,也就实现了全局负载均衡的概念。负载均衡示例 我们通过 NDS 访问数据中心对象存储上的静态资源为例,来看一看整个过程。 假设全国有多个数据中心,托管在多个运营商,每个数据中心有三个可用区。对象存储可以通过跨可用区部署,实现高可用性。在每个数据中心中,都至少部署两个内部负载均衡器,内部负载均衡器后面对接多个对象存储的前置服务器(Proxy-server)。那么,请求过程如下图:当一个客户端要访问 object.yourcompany.com 的时候,需要将域名转换为 IP 地址进行访问,所以它要请求本地 DNS 解析器;本地 DNS 解析器先查看本地的缓存是否有这个记录。如果有,就直接用,省略后续查询步骤,提高相应时间;如果本地无缓存,就需要请求本地的 DNS 服务器;本地 DNS 服务器一般部署在数据中心或者你所在的运营商网络中。本地 DNS 服务器也需要看本地是否有缓存,如果有,就直接返回;本地没有,通过第 5、6、7 步骤获取到 IP 地址,缓存到本地 DNS 解析器中,然后在返回给客户端。 对于不需要做全局负载均衡的简单应用来讲,yourcompany.com 的权威 DNS 服务器可以直接将 object.yourcompa.com 这个域名解析为一个或者多个 IP 地址,然后客户端可以通过多个 IP 地址,进行简单的轮询,实现简单的负载均衡。 但是对于复制的应用,尤其是跨地域跨运营商的大型应用,就需要更加复杂的全局负载均衡机制,因而需要专门的设备或者服务器来做这件事情,这就是全局负载均衡器(GSLB,Global Server Load Balance)。 在 yourcompany.com 的 DNS 服务器中,一般是通过配置 CNAME 的方式,给 object.yourcompany.com 起一个别名。例如 object.vip.yourcompany.com,然后告诉本地 DNS 服务器,让它请求 GSLB 解析这个域名,GSLB 就可以在解析这个域名的过程中,通过自己的策略实现负载均衡。上图中画了两层的 GSLB,是因为分运营商和地域。我们希望不同运营商的客户,可以访问对应运营商机房中的资源,这样不跨运营商访问,有利于提高吞吐量,减少时延。两层 GSLB 的过程如下:第一层 GSLB,通过查看请求它的本地 DNS 服务器所在的运营商,就知道用户所在的运营商。假设是移动,通过 CNAME 的方式,通过另一个别名 object.yd.yourcompany.com,告诉本地 DNS 服务器去请求第二层的 GSLB;第二层 GSLB,通过查看请求它的本地 DNS 服务器的地址,知道用户所在的地理位置,然后将距离用户位置比较近的一个 Region 的六个内部负载均衡的地址,返回给本地 DNS 服务器;本地 DNS 服务器将结果返回给本地 DNS 解析器;本地 DNS 解析器将结果缓存后,返回给客户端;客户端开始访问属于相同运营商的,且距离比较近的 Region1 中的对象存储。当然,客户端得到了六个 IP 地址,它可以通过负载均衡的方式,随机或者轮询选择一个可用区进行访问。对象存储一般会有三个备份,从而实现对存储读写的负载均衡。小结DNS 是网络世界的地址簿。可以通过域名查地址,因为域名服务器是按照树状结构组织的,因而域名查找是使用递归查询的方式,并通过缓存的方式加快效率;在域名和 IP 的映射中,给了应用基于域名做负载均衡的机会,可以是简单的负载均衡,也可以是根据地址和运营商做的全局负载均衡。参考:维基百科-域名系统 词条;知乎-域名解析;刘超 - 趣谈网络协议系列课; ...

December 26, 2018 · 2 min · jiezi

缓存使用

1,使用nginx代理缓存2,使用304状态码,springboot项目使用shadowEtagFilter3,使用springboot的enableCacheing注解实现缓存

December 24, 2018 · 1 min · jiezi

网络协议 15 - P2P 协议:小种子大学问

【前五篇】系列文章传送门:网络协议 10 - Socket 编程(上):实践是检验真理的唯一标准网络协议 11 - Socket 编程(下):眼见为实耳听为虚网络协议 12 - HTTP 协议:常用而不简单网络协议 13 - HTTPS 协议:加密路上无尽头网络协议 14 - 流媒体协议:要说爱你不容易 “兄弟,有种子吗?” “什么种子?小麦种吗?” “……,来,哥今天带你认识下什么是种子”。 大家说起种子,应该都知道是用来下载资源的。那么资源下载都有哪些方式?种子下载又有什么优势呢?下载电影的两种方式 第一种是通过 HTTP 进行下载。这种方式,有过经历的人应该体会到,当下载文件稍大点,下载速度简直能把人急死。 第二种方式就是是通过 FTP(文件传输协议)。FTP 采用两个 TCP 连接来传输一个文件。控制连接。服务器以被动的方式,打开众所周知用于 FTP 的端口 21,客户端则主动发起连接。该连接将命令从客户端传给服务器,并传回服务器的应答。常用的命令有:lsit - 获取文件目录,reter - 取一个文件,store - 存一个文件;数据连接。每当一个文件在客户端与服务器之间传输时,就创建一个数据连接。FTP 的工作模式 在 FTP 的两个 TCP 连接中,每传输一个文件,都要新建立一个数据连接。基于这个数据连接,FTP 又有两种工作模式:主动模式(PORT)和被动模式(PASV),要注意的是,这里的主动和被动都是站在服务器角度来说的。工作模式过程如下:主动模式工作流程客户端随机打开一个大于 1024 的端口 N,向服务器的命令端口 21 发起连接,同时开放 N+1 端口监听,并向服务器发出“port N+1” 命令;由服务器从自己的数据端口 20,主动连接到客户端指定的数据端口 N+1。被动模式工作流程客户端在开启一个 FTP 连接时,打开两个任意的本地端口 N(大于1024)和 N+1。然后用 N 端口连接服务器的 21 端口,提交 PASV 命令;服务器收到命令,开启一个任意的端口 P(大于 1024),返回“227 entering passive mode”消息,消息里有服务器开放的用来进行数据传输的端口号 P。客户端收到消息,取得端口号 P,通过 N+1 端口连接服务器的 P 端口,进行数据传输。 上面说了 HTTP 下载和 FTP 下载,这两种方式都有一个大缺点-难以解决单一服务器的带宽压力。因为它们使用的都是传统 C/S 结构,这种结构会随着客户端的增多,下载速度越来越慢。这在当今互联网世界显然是不合理的,我们期望能实现“下载人数越多,下载速度不变甚至更快”的愿望。 后来,一种创新的,称为 P2P 的方式实现了我们的愿望。P2P P2P 就是 peer-to-peer。这种方式的特点是,资源一开始并不集中存储在某些设备上,而是分散地存储在多台设备上,这些设备我们称为 peer。 在下载一个文件时,只要得到那些已经存在了文件的 peer 地址,并和这些 peer 建立点对点的连接,就可以就近下载文件,而不需要到中心服务器上。一旦下载了文件,你的设备也就称为这个网络的一个 peer,你旁边的那些机器也可能会选择从你这里下载文件。 通过这种方式解决上面 C/S 结构单一服务器带宽压力问题。如果使用过 P2P2 软件,例如 BitTorrent,你就会看到自己网络不仅有下载流量,还有上传流量,也就是说你加入了这个 P2P 网络,自己可以从这个网络里下载,同时别人也可以从你这里下载。这样就实现了,下载人数越多,下载速度越快的愿望。种子文件(.torent) 上面整个过程是不是很完美?是的,结果很美好,但为了实现这个美好,我们还是有很多准备工作要做的。比如,我们怎么知道哪些 peer 有某个文件呢? 这就用到我们常说的种子(.torrent)。 .torrent 文件由Announce(Tracker URL)和文件信息两部分组成。 其中,文件信息里有以下内容:Info 区:指定该种子包含的文件数量、文件大小及目录结构,包括目录名和文件名;Name 字段:指定顶层目录名字;每个段的大小:BitTorrent(BT)协议把一个文件分成很多个小段,然后分段下载;段哈希值:将整个种子种,每个段的 SHA-1 哈希值拼在一起。 下载时,BT 客户端首先解析 .torrent 文件,得到 Tracker 地址,然后连接 Tracker 服务器。Tracker 服务器回应下载者的请求,将其他下载者(包括发布者)的 IP 提供给下载者。 下载者再连接其他下载者,根据 .torrent 文件,两者分别对方自己已经有的块,然后交换对方没有的数据。 可以看到,下载的过程不需要其他服务器参与,并分散了单个线路上的数据流量,减轻了服务器的压力。 下载者每得到一个块,需要算出下载块的 Hash 验证码,并与 .torrent 文件中的进行对比。如果一样,说明块正确,不一样就需要重新下载这个块。这种规定是为了解决下载内容的准确性问题。 从这个过程也可以看出,这种方式特别依赖 Tracker。Tracker 需要收集所有 peer 的信息,并将从信息提供给下载者,使下载者相互连接,传输数据。虽然下载的过程是非中心化的,但是加入这个 P2P 网络时,需要借助 Tracker 中心服务器,这个服务器用来登记有哪些用户在请求哪些资源。 所以,这种工作方式有一个弊端,一旦 Tracker 服务器出现故障或者线路被屏蔽,BT 工具就无法正常工作了。那能不能彻底去中心化呢?答案是可以的。去中心化网络(DHT) DHT(Distributed Hash Table),这个网络中,每个加入 DHT 网络的人,都要负责存储这个网络里的资源信息和其他成员的联系信息,相当于所有人一起构成了一个庞大的分布式存储数据库。 而 Kedemlia 协议 就是一种著名的 DHT 协议。我们来基于这个协议来认识下这个神奇的 DHT 网络。 当一个客户端启动 BitTorrent 准备下载资源时,这个客户端就充当了两个角色:peer 角色:监听一个 TCP 端口,用来上传和下载文件。对外表明我这里有某个文件;DHT Node 角色:监听一个 UDP 端口,通过这个角色,表明这个节点加入了一个 DHT 网络。 在 DHT 网络里面,每一个 DHT Node 都有一个 ID。这个 ID 是一个长字符串。每个 DHT Node 都有责任掌握一些“知识”,也就是文件索引。也就是说,每个节点要知道哪些文件是保存哪些节点上的。注意,这里它只需要有这些“知识”就可以了,而它本身不一定就是保存这个文件的节点。 当然,每个 DHT Node 不会有全局的“知识”,也就是说它不知道所有的文件保存位置,只需要知道一部分。这里的一部分,就是通过哈希算法计算出来的。Node ID 和文件哈希值 每个文件可以计算出一个哈希值,而 DHT Node 的 ID 是和哈希值相同长度的串。 对于文件下载,DHT 算法是这样规定的:如果一个文件计算出一个哈希值,则和这个哈希值一样的那个 DHT Node,就有责任知道从哪里下载这个文件,即便它自己没保存这个文件。 当然不一定总这么巧,都能找到和哈希值一模一样的,有可能文件对应的 DHT Node 下线了,所以 DHT 算法还规定:除了一模一样的那个 DHT Node 应该知道文件的保存位置,ID 和这个哈希值非常接近的 N 个 DHT Node 也应该知道。 以上图为例。文件 1 通过哈希运算,得到匹配 ID 的 DHT Node 为 Node C(当然还会有其他的,为了便于理解,咱们就先关注 Node C),所以,Node C 就有责任知道文件 1 的存放地址,虽然 Node C 本身没有存放文件 1。 同理,文件 2 通过哈希计算,得到匹配 ID 的 DHT Node 为 Node E,但是 Node D 和 E 的值很近,所以 Node D 也知道。当然,文件 2 本身不一定在 Node D 和 E 这里,但是我们假设 E 就有一份。 接下来,一个新节点 Node new 上线了,如果要下载文件 1,它首先要加入 DHT 网络。如何加入呢? 在这种模式下,种子 .torrent 文件里面就不再是 Tracker 的地址了,而是一个 list 的 Node 地址,所有这些 Node 都是已经在 DHT 网络里面的。当然,随着时间的推移,很有可能有退出的,有下线的,这里我们假设,不会所有的都联系不上,总有一个能联系上。 那么,Node new 只要在种子里面找到一个 DHT Node,就加入了网络。 Node new 不知道怎么联系上 Node C,因为种子里面的 Node 列表里面很可能没有 Node C,但是没关系,它可以问。DHT 网络特别像一个社交网络,Node new 会去它能联系上的 Node 问,你们知道 Node C 的联系方式吗? 在 DHT 网络中,每个 Node 都保存了一定的联系方式,但是肯定没有所有 Node 的联系方式。节点之间通过相互通信,会交流联系方式,也会删除联系方式。这和人们的沟通方式一样,你有你的朋友圈,他有他的朋友圈,你们互相加微信,就互相认识了,但是过一段时间不联系,就可能会删除朋友关系一样。 在社交网络中,还有个著名的六度理论,就是说社交网络中的任何两个人的直接距离不超过六度,也就是即使你想联系比尔盖茨,最多通过六个人就能够联系上。 所以,Node New 想联系 Node C,就去万能的朋友圈去问,并且求转发,朋友再问朋友,直到找到 C。如果最后找不到 C,但是能找到离 C 很近的节点,也可以通过 C 的相邻节点下载文件 1。 在 Node C上,告诉 Node new,要下载文件 1,可以去 B、D、F,这里我们假设 Node new 选择了 Node B,那么新节点就和 B 进行 peer 连接,开始下载。它一旦开始下载,自己本地也有文件 1 了,于是,Node new 就告诉 C 以及 C 的相邻节点,我也有文件 1 了,可以将我加入文件 1 的拥有者列表了。 你可能会发现,上面的过程中漏掉了 Node new 的文件索引,但是根据哈希算法,一定会有某些文件的哈希值是和 Node new 的 ID 匹配的。在 DHT 网络中,会有节点告诉它,你既然加入了咱们这个网络,也就有责任知道某些文件的下载地址了。 好了,完成分布式下载了。但是我们上面的过程中遗留了两个细节性的问题。1)DHT Node ID 以及文件哈希值是什么? 其实,我们可以将节点 ID 理解为一个 160bits(20字节)的字符串,文件的哈希也使用这样的字符串。2)所谓 ID 相似,具体到什么程度算相似? 这里就要说到两个节点距离的定义和计算了。 在 Kademlia 网络中,两个节点的距离采用的是逻辑上的距离,假设节点 A 和 节点 B 的距离为 d,则:d = A XOR B 上面说过,每个节点都有一个哈希 ID,这个 ID 由 20 个字符,160 bits 位组成。这里,我们就用一个 5 bits ID 来举例。 我们假设,节点 A 的 ID 是 01010,节点 B 的 ID 是 01001,则:距离 d = A XOR B = 01010 XOR 00011 = 01001 = 9 所以,我们说节点 A 和节点 B 的逻辑距离为 9。 回到我们上面的问题,哈希值接近,可以理解为距离接近,也即,和这个节点距离近的 N 个节点要知道文件的保存位置。 要注意的是,这个距离不是地理位置,因为在 Kademlia 网络中,位置近不算近,ID 近才算近。我们可以将这个距离理解为社交距离,也就是在朋友圈中的距离,或者社交网络中的距离。这个和你的空间位置没有多少关系,和人的经历关系比较大。DHT 网络节点关系的维护 就像人一样,虽然我们常联系的只有少数,但是朋友圈肯定是远近都有。DHT 网络的朋友圈也一样,远近都有,并且按距离分层。 假设某个节点的 ID 为 01010,如果一个节点的 ID,前面所有位数都与它相同,只有最后 1 位不停,这样的节点只有 1 个,为 01011。与基础节点的异或值为 00001,也就是距离为 1。那么对于 01010 而言,这样的节点归为第一层节点,也就是k-buket 1。 类似的,如果一个节点的 ID,前面所有位数和基础节点都相同,从倒数第 2 位开始不同,这样的节点只有 2 个,即 01000 和 01001,与基础节点的亦或值为 00010 和 00011,也就是距离为 2 和 3。这样的节点归为第二层节点,也就是k-bucket 2。 所以,我们可以总结出以下规律:如果一个节点的 ID,前面所有位数相同,从倒数第 i 位开始不同,这样的节点只有 2^(i-1) 个,与基础节点的距离范围为 [2^(i-1), 2^i],对于原始节点而言,这样的节点归为k-bucket i。 你会发现,差距越大,陌生人就越多。但是朋友圈不能把所有的都放下,所以每一层都只放 K 个,这个 K 是可以通过参数配置的。DHT 网络中查找好友 假设,Node A 的 ID 为 00110,要找 B(10000),异或距离为 10110,距离范围在 [2^4, 2^5),这就说明 B 的 ID 和 A 的从第 5 位开始不同,所以 B 可能在 k-bucket 5 中。 然后,A 看看自己的 k-bucket 5 有没有 B,如果有,结束查找。如果没有,就在 k-bucket 5 里随便找一个 C。因为是二进制,C、B 都和 A 的第 5 位不停,那么 C 的 ID 第5 位肯定与 B 相同,即它与 B 的距离小于 2^4,相当于 A、B 之间的距离缩短了一半以上。 接着,再请求 C,在 C 的通讯里里,按同样的查找方式找 B,如果 C 找到了 B,就告诉 A。如果 C 也没有找到 B,就按同样的搜索方法,在自己的通讯里里找到一个离 B 更近一步的 D(D、B 之间距离小于 2^3),把 D 推荐给 A,A 请求 D 进行下一步查找。 你可能已经发现了,Kademlia 这种查询机制,是通过折半查找的方式来收缩范围,对于总的节点数目为 N 的网络,最多只需要 log2(N) 次查询,就能够找到目标。 如下图,A 节点找 B 节点,最坏查找情况: 图中过程如下:A 和 B 的每一位都不一样,所以相差 31,A 找到的朋友 C,不巧正好在中间,和 A 的距离是 16,和 B 的距离是 15;C 去自己朋友圈找,碰巧找到了 D,距离 C 为 8,距离 B 为 7;D 去自己朋友圈找,碰巧找到了 E,距离 D 为 4,距离 B 为 3;E 在自己朋友圈找,找到了 F,距离 E 为 2,距离 B 为 1;F 在距离为 1 的地方找到了 B。节点的沟通 在 Kademlia 算法中,每个节点下面 4 个指令:PING:测试一个节点是否在线。相当于打个电话,看还能打通不;STORE:要钱一个节点存储一份数据;FIND_NODE:根据节点 ID 查找一个节点;FIND_VALUE:根据 KEY 查找一个数据,实则上和 FIND_NODE 非常类似。KEY 就是文件对应的哈希值,找到保存文件的节点。节点的更新 整个 DHT 网络,会通过相互通信,维护自己朋友圈好友的状态。每个 bucket 里的节点,都按最后一次接触时间倒序排列。相当于,朋友圈里最近联系的人往往是最熟的;每次执行四个指令中的任意一个都会触发更新;当一个节点与自己接触时,检查它是否已经在 k-bucket 中。就是说是否已经在朋友圈。如果在,那么就将它移到 k-bucket 列表的最底,也就是最新的位置(刚联系过,就置顶下,方便以后多联系)。如果不在,就要考虑新的联系人要不要加到通讯录里面。假设通讯录已满,就 PING 一下列表最上面的节点(最旧的),如果 PING 通了,将旧节点移动到列表最底,并丢弃新节点(老朋友还是要留点情面的)。如 PING 不同,就删除旧节点,并将新节点加入列表(联系不上的老朋友还是删掉吧)。 通过上面这个机制,保证了任意节点的加入和离开都不影响整体网络。小结下载一个文件可以通过 HTTP 或 FTP。这两种都是集中下载的方式,而 P2P 则换了一种思路,采用非中心化下载的方式;P2P 有两种。一种是依赖于 Tracker 的,也就是元数据集中,文件数据分散。另一种是基于分布式的哈希算法,元数据和文件数据全部分散。参考:维基百科-DHT 网络词条;维基百科-Kademlia 词条;刘超 - 趣谈网络协议系列课; ...

December 24, 2018 · 4 min · jiezi

【总结】我们的2018年的关键词-坚持学习

因为涉及业务敏感话题,本文只记录我们学习的历程。关于坚持 从2016年起,我们团队坚持每天早晨8:50-10:30的100分钟早晨讨论,到现在已经两年了,期间没有中断过。我由衷的感谢团队的小伙伴们,感谢你们的坚韧不拔,感谢你们的持续成长。 回想一下,为什么开始学习活动,还是因为团队一个姑娘的一句话:“雷哥,我感觉我工作2年了,天天都是在用PHP写业务逻辑,虽然很努力很累,但是感觉自己也没什么成长”。 我问她“你天天用PHP,那你了解它工作的原理吗”,她摇摇头。 那好吧,我们就从PHP源码学起,大家自愿参加,开始了学习的历程。 从开始,不少同学完全看不懂c代码,到读代码很顺畅,到读代码很流利,到写c代码跟写php一样,我感受到了奇迹的变化。这个变化只是在每天的坚持和刻意练习中发生的,是那么的神奇。在这个过程中,我们定了一个目标,写几本关于我们常用软件源码的书。一年的收获 5月份我们的第一本书《PHP7底层设计与源码分析》出版,完成了2016年的一个小目标,虽然由于第一次出版书,不太会跟出版社配合导致印刷上有些问题,以及我们对内容的理解上还有些瑕疵(这里是勘误),但我很欣慰,感谢团队小伙伴们的坚持,感谢黄桃,李长林、李志、王坤、肖涛、朱栋跟我一起坚持。 回想一下,虽然每天的工作非常繁忙,但大家自愿牺牲掉了周末休息的时间,在一起校稿,一起讨论,慢慢的兄弟们对PHP7的内核越来越熟悉,虽然辛苦,但我们成长很多,兄弟的感情也越来越深,总之一切都是值得的。 书出版后,我在朋友圈发了一条动态,没想到兄弟姐妹们就给刷了屏,感谢各位小姐姐小哥哥! 看着这些朋友们的支持,我感动的哭了,我更为团队小伙伴们的坚持精神感动的哭了! 完成了第一本书,我们开始了Redis源码、Nginx源码、Beanstalkd源码的阅读,开始了对GoLang的探讨,大家依旧不论春夏秋冬,不论刮风下雨,依然保持每天的早读会:棉袄变成了长衫,长衫变成了短袖,短袖又变成了长袖,长袖又变成了大衣;听众变成了讲师,菜鸟变成了大神,害羞的她变成了敢于当众讲话的女神;今年冬天很冷,但依然挡不住我们火热的内心一切都在潜移默化中改变着。兄弟们的技术在精进,也能更好的服务于业务和工作。关于输出1、2月2日我们在思否(segmentfault)上开通了博客,一年来贡献了71篇文章,不少文章被点赞和收藏; 粉丝也从0涨到了550;2、我们建立了LNMPR源码交流的微信群,大家在群里一起讨论问题;3、我们利用周末时间编写了《Redis5命令设计与源码分析》这本书,预计春节后出版,目前已经完成了初稿的编写;4、我们研究了一遍Nginx的源码,准备元旦后开始第二遍研究及Nginx源码书籍的编写;5、一年来我们坚持研究和学习,这件事成了让我们开心和快乐的事情。6、我们也坚持写笔记:我想说的每天一点成长,积聚起来就是巨大的收获;永远都要有学习的习惯,把成长作为最重要的事;对于技术问题,要打破沙锅问到底,越问会发现自己不知道的越多;人类对事物的认知是从无知->知道->了解->熟悉->掌握->精通6个阶段, 坚持学习是唯一的道路;希望更多的人一起来坚持学习,一起来坚持成长。学习源码对业务的作用曾经有不少人问我,学习源码有什么用呢?能对业务产生多大的价值? 我总是会举一个91年小伙子的故事,3年前他来到团队,刚工作1年多,技术底子还是比较薄弱的,写业务代码问题不大,但对于线上问题基本是束手无策,有时候甚至只能放弃一些问题的跟进。 但2年过去了,他成长为了核心开发,线上部署,架构设计,以及线上问题定位都玩的转,线上出coredump了他总能第一个定位到原因。 这只是源码学习其中的一个意义而已。

December 21, 2018 · 1 min · jiezi

macos,使用nginx设置mysql反向代理

应用场景:服务器端程序异常,需要在本地搭建测试环境,并将服务器上的数据库同步到本地测试环境。遇到的问题:navicat备份的时候,会根据操作系统不同,生成不同的备份文件。所以由服务器复制下来的备份文件,不能够在本地的MACOS上直接还原。解决方案:在本地的其它windows电脑上,将备份的数据还原到MACOS上。实施步骤:安装nginx使用nginx进行反向代理,将本地的3306端口映射到3305端口。配置如下:http { # xxx}stream { upstream mysql { hash $remote_addr consistent; server 127.0.0.1:3306 max_fails=3 fail_timeout=30s; } server { listen 3305; proxy_connect_timeout 30s; proxy_timeout 600s; proxy_pass mysql; }}此时,我们使用与macos处于同一个局域网的电脑,打开navicat,设置访问的地址为 MACOS电脑IP,端口设置为3305,即可访问。当然也可以使用navicat的还原功能,来快速还原数据库了。

December 21, 2018 · 1 min · jiezi

将 vue spa 项目运行在 docker 的 nginx 容器中

将vue spa项目运行在docker的nginx容器中,步骤:1.安装docker2.下载nginx镜像([:tag]:是具体的nignx版本,比如::1.15.7;默认从 https://hub.docker.com/ 下载镜像):docker pull nginx[:tag]3.运行命令打包项目:npm run build4.编写nginx的配置文件(文件在本项目中位置:nginx/default.conf)5.在当前目录下运行 docker 命令([:tag]部分,需要替换成具体的值):docker run -p 9081:80 -v $PWD/dist/:/usr/share/nginx/dist/ -v $PWD/nginx/default.conf:/etc/nginx/conf.d/default.conf -d nginx[:tag]6.宿主机(就是本机)访问项目网址:http://localhost:9081/docker run命令参数说明:参数说明-v, –volume value:Bind mount a volume (default [])宿主机会覆盖容器内文件-p, –publish value:Publish a container’s port(s) to the host (default [])宿主机端口对应容器内端口-d, –detach:Run container in background and print container ID保持容器在后台持续运行;后续可以使用docker exec -it <容器名或容器id> bash,进入容器的bash命令项目例子:https://github.com/cag2050/vu…

December 20, 2018 · 1 min · jiezi

Centos 7离线安装Nginx 配置负载均衡集群

场景项目中有三台应用服务器,系统为Centos 7 ,应用地址分别为:192.168.198.229:8080192.168.198.230:8080192.168.198.231:8080应用使用tomcat部署,目前没有域名,都是使用IP在局域网中单独访问。因为没有单独的服务器可以用来部署Nginx,所以Nginx部署在229服务器上。安装依赖包在安装Nginx前,需要先安装好一些依赖包。gcc依赖包gcc-4.8.5-16.el7.x86_64.rpmglibc-devel-2.17-196.el7.x86_64.rpmglibc-headers-2.17-196.el7.x86_64.rpmkernel-headers-3.10.0-693.el7.x86_64.rpm其它依赖包pcre-devel-8.32-17.el7.x86_64.rpmzlib-devel-1.2.7-17.el7.x86_64.rpmopenssl-fips-2.0.10.tar.gz因为无法使用yum,我下载好后通过ftp上传到服务器。依赖包下载传送门:https://centos.pkgs.org/前四个为gcc安装包与相关依赖,最后一个openssl-fips如果使用rpm,还需要安装很多依赖包,因此使用压缩包安装更简单。gcc安装gcc安装验证: 其它依赖包安装[root@APP1 opt]# rpm -ivh pcre-devel-8.32-17.el7.x86_64.rpm 警告:pcre-devel-8.32-17.el7.x86_64.rpm: 头V3 RSA/SHA256 Signature, 密钥 ID f4a80eb5: NOKEY准备中… ################################# [100%]正在升级/安装…[root@APP1 opt]# rpm -ivh zlib-devel-1.2.7-17.el7.x86_64.rpm 警告:zlib-devel-1.2.7-17.el7.x86_64.rpm: 头V3 RSA/SHA256 Signature, 密钥 ID f4a80eb5: NOKEY准备中… ################################# [100%]正在升级/安装… 1:zlib-devel-1.2.7-17.el7 ################################# [100%] [root@APP1 opt]# tar zxvf openssl-fips-2.0.10.tar.gz [root@APP1 opt]# cd openssl-fips-2.0.10/[root@APP1 openssl-fips-2.0.10]# ./config && make && make install安装Nginx安装好上述依赖包后就可以安装Nginx了。安装如下:使用tar将nginx-1.12.0.tar.gz 解压到 /usr/local/目录,编译安装[root@HMDMAPP1 opt]# tar -zxvf nginx-1.12.0.tar.gz -C /usr/local/[root@HMDMAPP1 opt]# cd /usr/local/nginx-1.12.0/[root@HMDMAPP1 nginx-1.12.0]# ./configure && make && make install[root@HMDMAPP1 nginx-1.12.0]# whereis nginxnginx: /usr/local/nginx配置Nginx安装好后我们需要对Nginx进行配置。配置文件路径为:/usr/local/nginx/sconf/nginx.conf主要配置点:1、upstream这里配置一组被代理的服务器地址upstream mysvr { server 192.168.198.229:8080 weight=1 max_fails=3 fail_timeout=15; server 192.168.198.230:8080 weight=1 max_fails=3 fail_timeout=15; server 192.168.198.231:8080 weight=1 max_fails=3 fail_timeout=15; } 2、serverserver { listen 80; #监听端口,与应用端口不同 server_name 192.168.198.229; #监听地址,一般是配置域名 #charset koi8-r; #access_log logs/host.access.log main; location / { proxy_pass http://mysvr; #请求转向upstream配置中mysvr定义的服务器列表 }} 请求转向还有另外一种写法:如果upstream 中的服务器列表地址前加了http:// 则在server中的请求转向地址mysvr不需要加http://upstream mysvr{ server http://192.168.198.229:8080 weight=1 max_fails=3 fail_timeout=15; … …}server{ …. location / { proxy_pass mysvr; #请求转向upstream配置中mysvr定义的服务器列表 }}启动Nginx[root@HMDMAPP1 /]# cd /usr/local/nginx/sbin[root@HMDMAPP1 sbin]# ./nginxNginx常用命令查看进程: ps -aux |grep ’nginx’重启nginx: ./nginx -s reopen 停止nginx: ./nginx -s stop重新载入配置文件: ./nginx -s reload通过 192.168.198.229+应用地址 进行访问,我们可以在不同的服务器中的页面中添加标识来测试Nginx配置是否成功。下面访问test3.html页面不同刷新显示结果如下:可以看到访问地址没有变化,但Nginx把请求分配到了不同的服务器上。本文中使用到了依赖包与Nginx.conf完整配置文件下载:https://download.csdn.net/dow…推荐学习:Nginx部署与配置 ...

December 19, 2018 · 1 min · jiezi

centos nginx下配置免费https

准备记录下部署免费https的过程 ,使用Let’s Encrypt的免费证书下载自动安装脚本wget https://dl.eff.org/certbot-autochmod a+x certbot-auto安装执行脚本./certbot-auto –nginx这里会下载一些东西,让后让你选择你需要加https的域名,这个域名是在nginx.conf中配置的server中读取到的。注意选择的域名只能是备案过的那个域名。因为这个脚本会到DNS服务器去查这个域名对应的ip服务器。对不上也是不会给颁发证书的。执行成功后,会让你选择是否把http的请求重定向到https。直接选择2就行到这里已经配置成功了,访问下网站就可以看效果了。点击那个锁还可以看到关于证书的详细信息总结其实这脚本就是相当于一键安装包,帮你申请https证书,然后下载到服务器存放,然后在把证书配置到nginx.conf里边。打开nginx.conf就能看到新增的配置信息。 listen 443 ssl http2; # managed by Certbot ssl_certificate /etc/letsencrypt/live/fullchain.pem; # managed by Certbot ssl_certificate_key /etc/letsencrypt/live/privkey.pem; # managed by Certbot include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot # Redirect non-https traffic to https if ($scheme != “https”) { return 301 https://$host$request_uri; #该状态代码301告诉浏览器(和搜索引擎)这是永久重定向。这使浏览器记住重定向,以便下次访问时,浏览器将在内部进行重定向 }# managed by Certbot}附由于Let’s Encrypt这个证书90天后就过期了,可以使用cron做一个定时任务,因为我这个证书是18号申请的,所以每个月的19号就执行一次,执行crontab -e后会进入个文件输入0 0 19 * * ./path/to/certbot-auto renewcrontab -e的五个参数分别代表,分钟、小时、天、月、周。参考https://certbot.eff.org/lets-encrypt/centos6-nginxhttps://bjornjohansen.no/redirect-to-https-with-nginxhttp://nginx.org/en/docs/http/ngx_http_core_module.html

December 19, 2018 · 1 min · jiezi

Linux安装二进制PHP7.2

通过性能评测,可以看出PHP7对性能进行了较大的优化,相比与PHP5.x有50%-150%的性能提升,因此,为了提升我们服务的响应速度,降低机器负载,需要进行版本升级。因为对二进制比较熟悉,所以没有用yum的方式进行安装,采用的二进制安装方式比较灵活,但是因为第一次安装PHP的高版本,也引入了很多的问题,总而言之,就是在错误中不断摸索错误,最终找到一个还能用的道路。下载PHP7.2官方下载地址:wget http://cn2.php.net/get/php-7.2.13.tar.bz2/from/this/mirror -O php-7.2.13.tar.bz2tar -xjvf php-7.2.13.tar.bz2// 用于后面编译的生成代码目录mkdir php7cd php-7.2.13配置PHPPHP编译前提供了大量的参数进行配置,包括支持的扩展、执行用户等,可以查看参数列表。我们进行最简单的配置,只支持php-fpm管理,因为我们的PHP是配合Ngnix来进行服务,因此还要指定执行的用户:./configure –prefix=/home/work/lnmp/php7 –enable-fpm –with-fpm-user=nginx –with-fpm-group=nginx我的第一次编译报错:configure: error: OpenSSL version 1.0.1 or greater required.解决这个问题,需要首先看自己的openssl的版本信息:$ openssl versionOpenSSL 1.0.0-fips 29 Mar 2010因此更新openssl版本:wget https://www.openssl.org/source/openssl-1.1.0j.tar.gztar -xzvf openssl-1.1.0j.tar.gzcd openssl-1.1.0j./config –prefix=/usr/local/ssl shared zlib-dynamicmakemake installmv /usr/bin/openssl /usr/bin/openssl1.0.0ln -s /usr/local/ssl/bin/openssl /usr/bin/openssl安装完毕再次配置依然报相同错误,因此我们需要手动指定openssl的位置:// 查看指定openssl的参数$./configure –help | grep openssl –with-openssl=DIR Include OpenSSL support (requires OpenSSL >= 1.0.1) –with-openssl-dir=DIR FTP: openssl install prefix –with-openssl-dir=DIR SNMP: openssl install prefix$ ./configure –prefix=/home/work/lnmp/php7 –enable-fpm –with-fpm-user=nginx –with-fpm-group=nginx –with-openssl=/usr/bin/openssl安装 make && make install 启动因为我是升级,所以原有Nginx和代码以及配置文件都是OK的状态,可能在这个阶段你会遇到不同的问题,这个得结合你的情况进行解决。cd php7// 复制php.ini和php-fpm.conf到etc/目录下,这个过程你也可以自己配置啊// 生成两个目录用于日志和sock文件保存mkdir logmkdir runsbin/php-fpm -c etc/php.ini -y etc/php-fpm.conf -p .启动成功,访问URL,报错:502 Bad Gateway502 Bad Gateway根据nginx的访问日志可以看出:$ cat error.log2018/12/14 10:54:18 [crit] 6260#0: *206 open() “./run/factcgi_temp/0000000015” failed (13: Permission denied) while reading upstream, client: 172.24.162.178, server: , request: “GET /oss/index.php HTTP/1.1”, upstream: “fastcgi://unix:run/phpfpm.sock:”, host: “xx.xx.com"查阅【资料1】【资料2】可以知道,在PHP老版本里,有一个bug,任何能够连接socket文件的用户可以通过它执行任何命令,特别是在Ubuntu系统里允许www-data用户执行任何代码。因此最新版本里修复了这个错误,但也导致我们出现了502的问题,因此我们需要配套升级我们的配置文件:// 在nginx.conf头部添加执行用户user www www;// 在php-fpm.conf里放弃注释这3行; Set permissions for unix socket, if one is used. In Linux, read/write; permissions must be set in order to allow connections from a web server. Many; BSD-derived systems allow connections regardless of permissions.; Default Values: user and group are set as the running user; mode is set to 0666listen.owner = wwwlisten.group = wwwlisten.mode = 0660重启nginx和php-fpm进程,依然报错:nginx: [emerg] getpwnam(“www”) failed因为我们没有加上这个用户:useradd -r www搞定,重启nginx和php-fpm进程,服务正常。总结使用二进制来安装PHP7.2,在编译的时候按需加载扩展,如果有问题,我们可以重新编译,也可以动态扩展。过程比较简单,但我的服务并没有正常服务,因为使用的Yii2.0不能够完美兼容PHP7,我还得对Yii2.0进行升级,以及对自身的代码进行升级。参考资料PHP7.2下载地址:http://php.net/downloads.phpPHP的性能演进:http://www.laruence.com/2016/…OpenSSl downloads:https://www.openssl.org/source/OpenSSL 安装、介绍:https://www.jianshu.com/p/291…Centos7 安装 PHP7最新版:https://www.jianshu.com/p/246…CentOS 7 Linux 安装PHP7.2 - 编译安装:https://blog.csdn.net/ai_zxc/…nginx error connect to php5-fpm.sock failed (13: Permission denied):https://stackoverflow.com/que…nginx安装 nginx: [emerg] getpwnam(“www”) failed 错误:https://blog.csdn.net/justdoi… ...

December 18, 2018 · 1 min · jiezi

Nginx 是如何让你的缓存延期的

当 Nginx 使用 proxy cache 的文件作为响应时,它会更新其中的一些内容,比如 Date 响应头;但大部分响应头都不会得到更新,比如 Expires 和 Cache-Control。众所周知,Cache-Control 可以通过 max-age=xxx 或者 s-maxage=xxx 指令设置缓存的有效时间。跟 Expires 响应头不同,这一时间是相对的。假设上游服务器返回 Cache-Control: public; max-age=3600,那么 Nginx 会缓存该响应一小时。如果在这一小时到期之前,Client 访问了 Nginx,它会获取到同样的 Cache-Control 响应头,因此会再缓存多一小时。所以总体上该响应会被缓存两小时。这听起来很让人惊讶。但仔细想想,其实也不算什么严重的问题。首先,当我们设置 max-age=3600 时,大多数情况下并不要求其严格地在一小时后过期。其次,这个算是一般的多层缓存固有的弊端:缓存数据的最大过期时间,取决于各级缓存 TTL 的总和。如果想要避免,你可以选择根据外层数据剩下的 TTL 设置当前 TTL;或者提供主动 purge 的操作,从最外层开始逐层清理数据。当然,某些时候下,这一行为会带来一些问题。举个例子,假设我们开启了 proxy_cache_use_stale,在上游服务器出问题时使用过期的内容代替正常的响应。这种情况下,缓存只是作为一个临时救急的方案使用,我们并不希望 Client 多缓存更多的时间。否则会有上游应用的开发者抱怨,为何上游服务器已经正常了,用户刷新页面看到的还是旧数据。作为解决办法,我们可以在 Nginx 的 header filter 阶段,通过 Lua 代码或者 Nginx C module,把 Cache-Control: max-age=… 修改成 Cache-Control: no-cache。这么一来,Client 会在使用缓存之前先验证下,如果 Nginx 返回 304 状态码,那么该缓存会被继续使用;如果上游已经 OK 了且更新了响应,那么 Client 就会重新请求,避免使用过期的内容。这里需要强调下,no-cache 并非如字面上的意义表示不缓存,而是要求 Client 在使用该缓存之前,需要先验证下被缓存的内容是否还是最新的。MDN 的说法是:Forces caches to submit the request to the origin server for validation before releasing a cached copy.对应的,RFC 7234 的说法:The “no-cache” request directive indicates that a cache MUST NOT use a stored response to satisfy the request without successful validation on the origin server.如果要想让 Client 不缓存响应的内容,按 MDN 上的说法,需要用 Cache-Control: no-cache, no-store, must-revalidate(https://developer.mozilla.org…)。仔细看了下 no-cache / no-store / must-revalidate 这三项指令的介绍,似乎 no-store 就能让 Client 不用这个缓存,因为 no-store 要求:The cache should not store anything about the client request or server response.另外 must-revalidate 要求在使用过期缓存前验证下该内容是否是最新的,而 no-cache 也是要求重新验证的,那为什么需要两个都一起用呢?Google 搜索把我带到了这个 SO 问答:https://stackoverflow.com/que…。这个回答里面解释了为何不单单用 no-store:因为臭名昭著的 IE6 浏览器在处理 no-store 时有 bug。但可惜的是,这个回答没有给出这一论断的证据,比如 IE 的 bug report 之类。MDN 在给出 Cache-Control: no-cache, no-store, must-revalidate 这个例子的时候,也没有提及更多的上下文。这很像没有任何注释的老代码:我们不知道当初为何这么写,而把它删掉似乎不会带来什么问题。 ...

December 18, 2018 · 1 min · jiezi

在nginx中设置三级域名

问题描述通过配置nginx可以设置一个IP地址下面通过不同的端口访问不同的Web应用,但是时间长了之后端口号和应用之间的关系就很模糊了。 如 http://120.79.79.XX:9001 和foreign.XXX.xin 虽然这两个网址都是指向同一个网站,但是后者显然望文生义,比前者好很多。同时在网站SEO中,后者也比前者的权重更高。基本知识顶级域名:.com .cn二级域名:baidu.com sina.com ,这其中baidu 和sina就是二级域名三级域名:zhidao.baidu.com 其中zhidao就是三级域名基本步骤 设置地址解析 配置nginx 监听 配置nginx 跳转创建地址解析笔者使用的阿里云,登录到阿里云后台后,新增A记录,将三级域名名称填入到主机记录中,具体填写方法可以参考下图配置nginx修改 /etc/nginx/sites-aviablable中的default 文件,完整代码如下:server { listen 80 default_server; listen [::]:80 default_server; root /var/www/html/wordpress; index index.php index.html index.htm index.nginx-debian.html; server_name www.xXXX.xin; location / { try_files $uri $uri/ =404; } location ~ .php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php7.0-fpm.sock; } location ~ /.ht { deny all; }}#服务2server { listen 80; server_name foreign.XXX.xin; location / { proxy_pass http://120.79.XX.XX:9000/; }}两个服务都是监听的同一个端口80,但是服务2 的server_name 和新设置的地址解析保持一致。然后设定proxy_pass 将80端口获取到的信息转发到9000端口。 ...

December 17, 2018 · 1 min · jiezi

写给前端初学者:nginx 基本安装与配置总结

以下内容需要你掌握一些预备知识1.Ubuntu的目录结构2.SSH指令及FTP软件登录远程服务器3.Linux基本指令操作安装nginx安装之前搞清楚你的操作系统,Ubuntu还是CentOS,还有具体的版本。可以使用以下指令检测cat /proc/version建议操作系统都选择稳定版,如笔者使用的操作系统为Ubuntu Server16.04LTS。这种版本的问题一般都会少很多。安装的方法尽量在官方网站上面去看,一方面可以锻炼你的英语阅读能力;另一方面也比在百度上去找得到的信息要准确得多笔者目前的安装版本是 Ubuntu Server 16.04LTS ,官网地址通过简单地阅读能够快速地获取到以上的信息,但是考虑到Linux的权限问题应该在指令的前面加上sudo,否则安装失败(Ubuntu常用指令)sudo apt-get updatesudo apt-get install nginx 安装其他说明笔者在安装之前一般都会使用如下的指令先查看本机上是否已经安装dpkg -l |grep nginx查看的结果为安装完成之后的了解下nginx的安装位置(每个版本都可能存在差异,如果使用百度来搜索估计又要整晕)whereis nginx其中/usr/sbin/nginx 为执行指令所在位置/etc/nginx 为nginx配置文件所在位置如何部署代码进入/etc/nginx 文件夹,我们重点关注sites-available和sites-enabled翻译过来就是【可以启用的站点】和【已经启用的站点】使用FTP工具登录到服务器之后可以看到 sites-enabled 下面默认有一个default,但是上面有一个类似快捷方式的图标,实际上这是一个软链接,链接的文件在 sites-available中(用ubuntu ln 指令可以建立软链接)这也意味着实际上已经【sites-enabled】就是【sites-available】的软链接我们可以直接修改【sites-available】的default 来设置第一个站点,如下就是default 的设置使用vim指令打开的情况(如果不熟悉vim指令可以通过FTP工具把这个文件下载下来修改之后再上传)其中:1.listen 9999:表示监听9999端口2.root /var/www/html/bigDataweb :表示站点的目录放在/var/www/html/bigDataweb文件夹下面以上设置完成后,即可启动服务5.nginx 的指令sudo /usr/sbin/nginx -t //检查配置是否正确sudo /usr/sbin/nginx //启动服务sudo /usr/sbin/nginx -s reload //重新载入配置/usr/sbin/nginx 是使用whereis指令检查到的nginx的命令位置如果以上方法你都觉得不好用,就用Ubuntu 的reboot指令吧

December 17, 2018 · 1 min · jiezi

Java面试题:面向对象,类加载器,JDBC, Spring 基础概念

为什么说Java是一门平台无关语言?平台无关实际的含义是“一次编写到处运行”。Java 能够做到是因为它的字节码(byte code)可以运行在任何操作系统上,与底层系统无关。2. 为什么 Java 不是100%面向对象?Java 不是100%面向对象,因为它包含8个原始数据类型,例如 boolean、byte、char、int、float、double、long、short。它们不是对象。3. 什么是 singleton class,如何创建一个 singleton class?Singleton class 在任何时间同一个 JVM 中只有一个实例。可以把构造函数加 private 修饰符创建 singleton。4. 什么是多态?多态简单地说“一个接口,多种实现”。多态的出现使得在不同的场合同一个接口能够提供不同功能,具体地说可以让变量、函数或者对象能够提供多种功能。下面是多态的两种类型:编译时多态运行时多态编译时多态主要是对方法进行重载(overload),而运行时多态主要通过使用继承或者实现接口。什么是运行时多态,也称动态方法分配?在 Java 中,运行时多态或动态方法分配是一种在运行过程中的方法重载。在这个过程中,通过调用父类的变量引用被重载的方法。下面是一个例子:Java面试题:面向对象,类加载器,JDBC, Spring 基础概念5. Java类加载器包括几种?它们之间的关系是怎么样的?Java 类加载器有:引导类加载器(bootstrap class loader):只加载 JVM 自身需要的类,包名为 java、javax、sun 等开头。扩展类加载器(extensions class loader):加载 JAVA_HOME/lib/ext 目录下或者由系统变量 -Djava.ext.dir 指定位路径中的类库。应用程序类加载器(application class loader):加载系统类路径 java -classpath 或 -Djava.class.path 下的类库。自定义类加载器(java.lang.classloder):继承 java.lang.ClassLoader 的自定义类加载器。注意:-Djava.ext.dirs 会覆盖 Java 本身的 ext 设置,造成 JDK 内建功能无法使用。可以像下面这样指定参数:Java面试题:面向对象,类加载器,JDBC, Spring 基础概念它们的关系如下:启动类加载器,C++实现,没有父类。扩展类加载器(ExtClassLoader),Java 实现,父类加载器为 null。应用程序类加载器(AppClassLoader),Java 实现,父类加载器为 ExtClassLoader 。自定义类加载器,父类加载器为AppClassLoader。Java学习交流圈:834962734 ,进群可免费获取一份Java架构进阶技术精品视频。(高并发+Spring源码+JVM原理解析+分布式架构+微服务架构+多线程并发原理+BATJ面试宝典)6. 什么是JDBC驱动?JDBC Driver 是一种实现 Java 应用与数据库交互的软件。JDBC 驱动有下面4种:JDBC-ODBC bridge 驱动Native-API 驱动(部分是 Java 驱动)网络协议驱动(全部是 Java 驱动)Thin driver(全部是 Java 驱动)7. 使用 Java 连接数据库有哪几步?注册驱动类新建数据库连接新建语句(statement)查询关闭连接8. 列举Spring配置中常用的重要注解。下面是一些重要的注解:@Required@Autowired@Qualifier@Resource@PostConstruct@PreDestroy9. Spring中的Bean是什么?列举Spring Bean的不同作用域。Bean 是 Spring 应用的骨架。它们由 Spring IoC 容器管理。换句话说,Bean 是一个由 Spring IoC 容器初始化、装配和管理的对象。下面是 Spring Bean 的5种作用域:Singleton:每个容器只创建一个实例,也是 Spring Bean 的默认配置。由于非线程安全,因此确保使用时不要在 Bean 中共享实例变量,一面出现数据不一致。Prototype:每次请求时创建一个新实例。Request:与 prototype 相同,区别在于只针对 Web 应用。每次 HTTP 请求时创建一个新实例。Session:每次收到 HTTP 会话请求时由容器创建一个新实例。全局 Session:为每个门户应用(Portlet App)创建一个全局 Session Bean。

December 17, 2018 · 1 min · jiezi

从 node服务从部署,到https配置与nginx转发

从 node服务从部署,到https配置与nginx转发最近在搞小程序,小程序的服务必须使用https协议,之前没学过这些,于是写下这篇博客,记录自己遇到的问题本篇博客解决这些问题,服务器的登陆配置、项目的部署、https证书的申请、nginx部署https与转发本地服务通过私钥登陆服务器腾讯云重装系统登陆设置选择使用ssh密钥设置选择ssh密钥,如果没有则创建ssh密钥点击开始安装下载生成好的私钥到本地使用终端进行配置// 赋予私钥文件仅本人可读权限chmod 400 <下载的与云服务器关联的私钥的绝对路径>// 运行以下远程登录命令ssh -i <下载的与云服务器关联的私钥的绝对路径> <username>@<hostname or ip address>给服务器装nvm管理node版本// 下载nvmwget https://github.com/cnpm/nvm/archive/v0.23.0.tar.gz// 解压nvmtar -xf v0.23.0.tar.gz// 进入目录cd nvm-0.23.0/// 安装nvm./install.sh// 安装后执行source ~/.bash_profile使用nvm安装nodenvm install 10.14.2给服务器安装git由于我的代码托管在github上,给服务器安装git方便管理代码在linux安装git克隆项目到服务器git clone https://github.com/lfhwnqe/wechat_server.git进入项目根目录安装依赖cd wechat_servernpm install启动项目进行连接npm start现在项目在本7001端口启动此时访问服务器公网ip:7001就可以访问到服务器上启动的服务把域名映射到服务器进入域名管理界面(我用的是阿里的)点击解析设置点击修改在记录值处修改为服务器的公网ip保存设置,然后访问你的域名有服务监听80端口的话就可以得到响应了申请https证书先申请一个免费https证书在服务器运行一下命令,通过openssl生成csr和私钥openssl req -new -newkey rsa:2048 -sha256 -nodes -out linuoblog.cn.csr -keyout linuoblog.cn.key -subj “/C=CN/ST=ShenZhen/L=ShenZhen/O=NUO Inc./OU=Web Security/CN=linuoblog.cn” 下面是上述命令相关字段含义:C:Country ,单位所在国家,为两位数的国家缩写,如: CN 就是中国ST 字段: State/Province ,单位所在州或省L 字段: Locality ,单位所在城市 / 或县区O 字段: Organization ,此网站的单位名称;OU 字段: Organization Unit,下属部门名称;也常常用于显示其他证书相关信息,如证书类型,证书产品名称或身份验证类型或验证内容等;CN 字段: Common Name ,你的网站域名;如果你是使用https://freessl.cn获取证书,在使用openssl命令生成 csr 文件后,在页面选择csr生成,并粘贴生成的csr内容到页面,然后通过该网站的验证方式,验证所有者,即可获得ca证书https://freessl.cn获得的ca证书是 .pem后缀的证书文件,证书文获取成功后,就可以在 Nginx 配置文件里配置 HTTPS 了。配置nginx,域名启用https,通过nginx把https请求转发到本地node服务要开启 HTTPS 服务,在配置文件信息块(server block),必须使用监听命令 listen 的 ssl 参数和定义服务器证书文件和私钥文件,同时通过设置location模块把发送到网页的请求转发到服务器上的node服务,如下所示:worker_processes auto;http { #配置共享会话缓存大小,视站点访问情况设定 ssl_session_cache shared:SSL:10m; #配置会话超时时间 ssl_session_timeout 10m; server { listen 443 ssl; server_name linuoblog.cn; #设置长连接 keepalive_timeout 70; #HSTS策略 add_header Strict-Transport-Security “max-age=31536000; includeSubDomains; preload” always; #证书文件 ssl_certificate full_chain.pem; # 证书的路径 #私钥文件 ssl_certificate_key linuoblog.cn.key; # 私钥的路径 location / { # 这里是把链接代理到本机的7001端口 proxy_pass http://127.0.0.1:7001; } }} events { worker_connections 1024; ## Default: 1024 }配置完成后启动nginxnginx这个时候,访问自己的域名就能看到https服务了。同时也通过nginx把网页端的请求转发到了服务器本地的server上 ...

December 14, 2018 · 1 min · jiezi

网络协议 12 - HTTP 协议:常用而不简单

系列文章传送门:网络协议 1 - 概述网络协议 2 - IP 是怎么来,又是怎么没的?网络协议 3 - 从物理层到 MAC 层网络协议 4 - 交换机与 VLAN:办公室太复杂,我要回学校网络协议 5 - ICMP 与 ping:投石问路的侦察兵网络协议 6 - 路由协议:敢问路在何方?网络协议 7 - UDP 协议:性善碰到城会玩网络协议 8 - TCP 协议(上):性恶就要套路深网络协议 9 - TCP协议(下):聪明反被聪明误网络协议 10 - Socket 编程(上):实践是检验真理的唯一标准网络协议 11 - Socket 编程(下):眼见为实耳听为虚 网络协议五层通天路,咱们从物理层、到链路层、网络层、再到传输层,现在又进一步,来到了应用层。这也是我们五层协议里最上面的一层,关于应用层,有太多协议要了解。但要说最有名的,那肯定就是 HTTP 了。 HTTP 协议,几乎是每个人上网用的第一个协议,同时也是很容易被人忽略的协议。 就像 http://blog.muzixizao.com/,是个 URL,叫作统一资源定位符。之所以叫统一,是因为它是有规定格式的。HTTP 称为协议,blog.muzixizao.com 是一个域名,表示互联网的一个位置。有的 URL 会有更详细的位置标识,例如http://blog.muzixizao.com/?p=140 正是因为格式是统一的,所以当你把这样一个字符串输入到浏览器的框里的时候,浏览器才知道如何进行统一处理。HTTP 请求的准备 浏览器会将 blog.muzixizao.com 这个域名发送给 DNS 服务器,让它解析为 IP 地址。关于 DNS 解析的过程,较为复杂,后面会专门介绍。 域名解析成 IP 后,下一步是干嘛呢? 还记得吗?HTTP 是基于 TCP 协议的,所以接下来就是建立 TCP 连接了。具体的连接过程可点击这里查看。 目前使用的 HTTP 协议大部分都是 1.1.在 1.1 协议里面,默认开启了 Keep-Alive 的,这样建立的 TCP 连接,就可以在多次请求中复用。虽然 HTTP 用了各种方式来解决它存在的问题,但基于TCP 的它,每次建立连接的三次握手以及断开连接的四次挥手,这个过程还是挺费时的。如果好不容易建立了连接,然后做一点儿事情就结束了,未免太浪费了。HTTP 请求的构建 建立了连接以后,浏览器就要发送 HTTP 的请求。请求的格式如下图: 如图,HTTP 的报文大概分为请求行、首部、正文实体三部分。接下来,咱们就来一一认识。请求行 在请求行中,URL 就是 http://blog.muzixizao.com,版本为 HTTP 1.1。这里要说一下的,就是对应的请求方法。有以下几种类型:1)GET 请求 对于访问网页来讲,最常用的类型就是 GET。顾名思义,GET 就是去服务器获取一些资源。对于访问网页来讲,要获取的资源往往是一个页面。其实也有很多其他的格式,比如返回一个 JSON 字符串。当然,具体要返回什么,是由服务端决定的。 例如,在云计算中,如果我们的服务端要提供一个基于 HTTP 协议的 API,获取所有云主机的列表,就会使用 GET 方法请求,返回的可能是一个 JSON 字符串,字符串里面是一个列表,列表里面会有各个云主机的信息。2)POST 请求 另一种类型叫做 POST。它需要主动告诉服务端一些信息,而非获取。而要告诉服务端的信息,一般都放在正文里面。正文里有各种各样的格式,最常见的的就是 JSON了。 例如,我们平时的支付场景,客户端就需要把 “我是谁?我要支付多少?我要买什么?” 这样信息告诉服务器,这就需要 POST 方法。 再如,在云计算里,如果我们的服务器,要提供一个基于 HTTP 协议的创建云主机的 API,也会用到 POST 方法。这个时候往往需要将 “我要创建多大的云主机?多少 CPU 和多少内存?多大硬盘?” 这些信息放在 JSON 字符串里面,通过 POST 的方法告诉服务器。 除了上面常见的两种类型,还有一种 PUT 类型,这种类型就是向指定资源位置上传最新内容。但是 HTTP 的服务区往往是不允许上传文件的,所以 PUT 和 POST 就都变成了要传给服务器东西的方法。 在我们的实际使用过程中,PUT 和 POST 还是有区别的。POST 往往是用来创建一个资源,而 PUT 往往是用来更新一个资源。 例如,云主机已经创建好了,想对云主机打一个标签,说明这个云主机是生产环境的,另外一个云主机是测试环境的。我们修改标签的请求往往就是用 PUT 方法。 还有 DELETE 方法。这个是用来删除资源的。首部字段 请求行下面就是首部字段。首部是 key-value 格式,通过冒号分割。这里面,往往保存了一些非常重要的字段。Accpet-Charset:客户端可以接受的字符集。防止传过来的字符串客户端不支持,从而出现乱码;Content-Type:正文格式。我们进行 POST 请求时,如果正文是 JSON,我们就应该将这个值设置为 application/json;缓存字段 Cache-Control、If-Modified-Since。 这里重点认识下缓存字段。为什么要使用缓存呢?这是因为一个非常大的页面有很多东西。 例如,我们浏览一个商品的详情,里面有商品的价格、库存、展示图片、使用手册等待。 商品的展示图片会保持较长时间不变,而库存胡一根筋用户购买情况经常改变。如果图片非常大,而库存数非常小,如果我们每次要更新数据的时候都要刷新整个页面,对于服务器的压力也会很大。 对于这种高并发场景下的系统,在真正的业务逻辑之前,都需要有个接入层,将这些静态资源的请求拦在最外面。架构就像下图: 其中 DNS、CDN 会在后面的章节详细说明。这里咱们就先来了解下 Nginx 这一层。它是如果处理 HTTP 协议呢?对于静态资源,有 Vanish 缓存层,当缓存过期的时候,才会访问真正的 Tomcat 应用集群。 在 HTTP 头里面,Cache-Control 是用来控制缓存的。当客户端发送的请求中包含 max-age 指令时,如果判定缓存层中,资源的缓存时间数值比指定时间的数值校,那么客户端可以接受缓存的资源;当指定 max-age 值为 0,那么缓存层通常需要将请求转发给应用集群。 另外,If-Modified-Since 也是关于缓存的字段,这个字段是说,如果服务器的资源在某个时间之后更新了,那么客户端就应该下载最新的资源;如果没有更新,服务端会返回“304 Not Modified” 的响应,那客户端就不用下载了,也会节省带宽。 到此,我们拼凑起了 HTTP 请求的报文格式,接下来,浏览器会把它交给传输层。HTTP 请求的发送 HTTP 协议是基于 TCP 协议的,所以它是以面向连接的方式发送请求,通过 stream 二进制流的方式传给对方。当然,到了 TCP 层,它会把二进制流变成一个个的报文段发送给服务器。 在发送给每个报文段的时候,都需要对方有一个回应 ACK,来保证报文可靠地到达了地方。如果没有回应,那么 TCP 这一层会重新传输,直到可以到达。同一个包有可能被传了好多次,但是 HTTP 这一层不需要知道这一点,因为是 TCP 这一层在埋头苦干。而后续传输过程如下:TCP 层封装目标地址和源地址。TCP 层发送每一个报文的时候,都需要加上自己的地址和它想要去的地址,将这两个信息放到 IP 头里面,交给 IP 层进行传输。IP 层获取 MAC 头。IP 层需要查看目标地址和自己是否在同一个局域网。如果是,就发送 ARP 协议来请求这个目标地址对应的 MAC 地址,然后将源 MAC 和目标 MAC 放入 MAC 头,发送出去;如果不在同一个局域网,就需要发送到网关,这里也要通过 ARP 协议来获取网关的 MAC 地址,然后将源 MAC 和网关 MAC 放入 MAC 头,发送出去。网关转发。网关收到包发现 MAC 符合,取出目标 IP 地址,根据路由协议找到下一跳的路由器,获取下一跳路由器的 MAC 地址,将包发给下一跳路由器。数据包到达目标地址的局域网。通过 ARP 协议获取目标地址的 MAC 地址,将包发出去。目标地址检查信息,返回 ACK。目标机器发现数据包中的 MAC 地址及 IP 地址都和本机匹配,就根据 IP 头中的协议类型,知道是 TCP 协议,解析 TCP 的头,获取序列号。判断序列号是否是本机需要的,如果是,就放入缓存中然后返回一个 ACK,如果不是就丢弃。根据端口号将数据包发送到指定应用。TCP 头里面还有端口号,HTTP 的服务器正在监听这个端口号。于是,目标机器自然指定是 HTTP 服务器这个进程想要这个包,就把数据包发给 HTTP 服务器。HTTP 服务器处理请求。HTTP 服务器根据请求信息进行处理,并返回数据给客户端。HTTP 返回的构建 HTTP 的返回报文也是有一定格式的,如下图:状态行包含状态码和短语。状态码反应 HTTP 请求的结果。200 是大吉大利;404 则是我们最不想见到的,也就是服务端无法响应这个请求。短语中会说明出错原因。首部 key-value。这里常用的有以下字段:Retry-After:客户端应该在多长时间后再次尝试连接;Content-Type:返回数据格式 构造好了返回的 HTTP 报文,接下来就是把这个报文发送出去。当然,还是交给 Socket 去发送,交给 TCP,让 TCP 返回的 HTML 分成一个个小的数据段,并且保证每一段都安全到达。这些小的数据段会加上 TCP 头,然后交给 IP 层,沿着来时的路反向走一遍。虽然不一定是完全相同的路径,但是逻辑过程是一样的,一直到达客户端。 客户端取出数据后 ,会根据端口号交给指定的程序,这时候就是我们的浏览器出马的时候。 浏览器拿到了 HTTP 报文,发现返回 200,一切正常,就从正文中将 HTML 拿出来,展示出一个炫酷吊炸天的网页。 以上就是正常的 HTTP 请求与返回的完整过程。HTTP 2.0 上面提到了,现在用到 HTTP 大多是 1.1 版本,而 HTTP 2.0 在 1.1 的基础上进行了一些优化,以期解决一些问题。 HTTP 1.1 在应用层以纯文本的形式进行通信。每次通信都要带完整的 HTTP 头,而且不考虑 pipeline 模式的话,每次的过程都要像上面描述的那样一去一回。显然,在效率上会存在问题。 为了解决这些问题,HTTP 2.0 会对 HTTP 头进行一定的压缩,将原来每次都要携带的大量 key-value 对在两端建立一个索引表,对相同的头只发送索引表中的索引。 另外,HTTP 2.0 协议将一个 TCP 连接切分成多个流,每个流都有自己的 ID,而且流可以是客户端发给服务端,也可以是服务端发给客户端,它其实只是个虚拟的通道,除此之外,它还有优先级。 HTTP 2.0 将所有的传输信息分割成更小的消息和帧,并对它们采用二进制格式编码。常见的帧有 Header 帧,用于传输 Header 内容,并且会开启一个新的流。还有 Data 帧,用来传输正文实体,并且多个 Data 帧属于同个流。 通过这两种机制,HTTP 2.0 的客户端可以将多个请求分到不同的流中, 然后将请求内容拆分成帧,进行二进制传输。这些帧可以打散乱序发送,然后根据帧首部的流标识符重新组装,并且可以根据优先级,决定先处理哪个流的数据。 针对 HTTP 2.0,我们来看一个例子。 假设我们有一个页面要发送三个独立的请求,一个获取 CSS、一个获取 JS、一个获取图片 jsg。如果使用 HTTP 1.1,这三个请求就是串行的,但是如果使用 HTTP 2.0,就可以在一个连接里,客户端和服务端同时反思多个请求和回应,而且不用按照顺序一对一对应。 如上图。HTTP 2.0 其实是将三个请求变成三个流,将数据分成帧,乱序发送到一个 TCP 连接中。 HTTP 2.0 成功解决了 HTTP 1.1 的队首阻塞问题。同时,也不需要通过 HTTP 1.x 的 pipeline 机制用多条 TCP 连接来实现并行请求与响应,减少了 TCP 连接数对服务器性能的影响,加快页面组件的传输速度。 HTTP 2.0 虽然大大增加了并发性,但由于 TCP 协议的按序处理的特性,还是会出现阻塞的问题。 还记得咱们之前说过的 QUIC 协议吗?这时候就是它登场的时候了。 它用以下四个机制,解决了 TCP 存在的一些问题。机制一:自定义连接机制 我们知道,一条 TCP 连接是由四元组标识的。一旦一个元素发生变化,就需要端口重连。这在移动互联网的情况下,当我们切换网络或者信号不稳定时,都会导致重连,从而增加时延。 TCP 没办法解决上述问题,但是 QUCI 基于 UDP 协议,就可以在自己的逻辑里面维护连接的机制,不再以四元组标识,而是以一个 64 位的随机数作为标识 ID,而且 UDP 是无连接的,只要 ID 不变,就不需要重新建立连接。机制二:自定义重传机制 TCP 为了保证可靠性,通过使用序号和应答机制,来解决顺序问题和丢包问题。 任何一个序号的包发出去,都要在一定时间内得到应答,否则就会超时重发。这个超时时间就是通过采样往返时间 RTT 不断调整的。其实,这个超时时间的采样是不太准确的。 如上图。发送一个包,序号为 100,超时后,再发送一个 100。然后收到了一个 ACK101。这个时候客户端知道服务器已经收到了 100,但是往返时间怎么计算呢?是 ACK 到达时间减去后一个 100 发送的时间,还是减去前一个 100 发送的时间呢?前者把时间算短了,后者把时间算长了。 QUIC 也有一个序列号,是完全递增的。任何一个包发送一次后,下一次序列号就要加一。像我们上面的例子,在 QUIC 协议中,100 的包没有返回,再次发送时,序号就是 101 了,如果返回是 ACK100,就是对第一个包的响应,如果返回 ACK101,就是对第二个包的响应,RTT 时间计算相对准确,过程如下图: 上面的过程中,有的童鞋可能会问了,两个序号不一样的包,服务器怎么知道是同样的内容呢?没错,这确实是个问题。为了解决这个问题,QUIC 协议定义了一个 Offset 的概念。 QUIC 既然是面向连接的,也就像 TCP 一样,是一个数据流。,发送的数据在这个流里面都有个偏移量 Offset,可以通过 Offset 查看数据发送到了那里,这样只要这个 Offset 的包没有来,就要重发。如果来了,就按照 Offset 拼接成一个流。机制三:无阻塞的多路复用 有了自定义的连接和重传机制,我们就可以解决上面 HTTP 2.0 的多路复用问题。 同 HTTP 2.0 一样,同一条 QUIC 连接上可以创建多个 stream,来发送多个 HTTP 请求。更棒的是,QUIC 是基于 UDP 的,一个连接上的多个 stream 之间没有依赖。这样,假如 stream2 丢了一个 UDP 包,后面跟着 stream3 的一个 UDP 包,虽然 stream2 的那个包需要重传,但是 stream3 的包无需等待,就可以发给用户。机制四:自定义流量控制 TCP 的流量控制是通过滑动窗口协议。QUIC 的流量控制也是通过 window_update,来告诉对端它可以接受的字节数。但是 QUIC 的窗口是适应自己的多路复用机制的,不但在一个连接上控制窗口,还在一个连接中的每个 stream 控制窗口。 还记得吗?在 TCP 协议中,接收端的窗口的起始点是下一个要接收并且 ACK 的包,即便后来的包都到了,放在缓存里面,窗口也不能右移,因为 TCP 的 ACK 机制是基于序列号的累计应答,一旦 ACK 一个序列号,就说明前面的都到了,所以只要前面的没到,后面的即使到了也不能 ACK,就会导致后面的到了,也有可能超时重传,浪费带宽。 QUIC 的 ACK 是基于 offset 的,每个 offset 的包来了,进了缓存,就可以应答,应答后就不会重发,中间的空档会等待到来或者重发即可,而窗口的起始位置为当前收到的最大 offset,从这个 offset 到当前的 stream 所能容纳的最大缓存,是真正的窗口大小,显然,这样更加准确。 另外,还有整个连接的窗口,需要对于所有的 stream 的窗口做一个统计。小结HTTP 协议虽然很常用,也很复杂,我们只需要重点记住 GET、POST、PUT、DELETE 这几个方法,以及重要的首部字段;HTTP 2.0 通过头压缩、分帧、二进制编码、多路复用等技术提升性能;QUIC 协议通过基于 UDP 自定义的类似 TCP 的连接、重试、多路复用、流量控制技术,进一步提升性能。参考:The TCP/IP Guide;百度百科 - HTTP 词条;刘超 - 趣谈网络协议系列课; ...

December 13, 2018 · 3 min · jiezi

【Nginx源码分析】Nginx中http2浅析

运营研发 张仕华本文通过一个小例子串一遍nginx处理http2的流程。主要涉及到http2的协议以及nginx的处理流程。http2简介http2比较http1.1主要有如下五个方面的不同:二进制协议http1.1请求行和请求头部都是纯文本编码,即可以直接按ascii字符解释,而http2是有自己的编码格式。并且nginx中http2必须建立在ssl协议之上。头部压缩举个例子,HTTP1.1传一个header <method: GET>,需要11个字符.http2中有一个静态索引表,客户端传索引键,例如1,nginx通过查表能知道1代表method: GET.nginx中除了该静态表,还会有一个动态表,保存例如host这种变化的头部多路复用http1.1一个连接上只能传输一个请求,当一个请求结束之后才能传输下一个请求。所以对http1.1协议的服务发起请求时,一般浏览器会建立6条连接,并行的去请求不同的资源。而http2的二进制协议中有一个frame的概念,每个frame有自己的id,所以一个连接上可以同时多路复用传输多个不同id的frame主动pushhttp1.1是请求-响应模型,而http2可以主动给客户端推送资源优先级既然多路复用,所有数据跑在了一条通道上,必然会有优先级的需求本文的例子主要通过解析报文说明头三个特性配置环境NGINX配置如下: server { listen 8443 ssl http2; access_log logs/host_server2.access.log main; ssl_certificate /home/xiaoju/nginx-2/nginx-selfsigned.crt; ssl_certificate_key /home/xiaoju/nginx-2/nginx-selfsigned.key; ssl_ciphers EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5; location / { root html; index index.html index.htm /abc.html; access_log logs/host_location3.access.log main; http2_push /favicon.ico; http2_push /nginx.png; } }客户端按如下方式发起请求:curl -k -I -L https://IP:8443HTTP/2 200 //可以看到,返回是http/2server: nginx/1.14.0date: Tue, 11 Dec 2018 09:20:33 GMTcontent-type: text/htmlcontent-length: 664last-modified: Tue, 11 Dec 2018 04:19:32 GMTetag: “5c0f3ad4-298"accept-ranges: bytes请求解析客户端请求问题先思考一个问题,上文配置中使用curl发送请求时,为何直接返回的是http/2,而不是http/1.1(虽然服务端配置了使用http2,但万一客户端未支持http2协议,直接返回http2客户端会解析不了)因为nginx中http2必须在ssl之上,所以我们首先通过在nginx代码中的ssl握手部分打断点gdb跟一下.(gdb) b ngx_ssl_handshake_handler //ssl握手函数Breakpoint 1 at 0x47ddb5: file src/event/ngx_event_openssl.c, line 1373.(gdb) cContinuing.Breakpoint 1, ngx_ssl_handshake_handler (ev=0x16141f0) at src/event/ngx_event_openssl.c:13731373 {1390 c->ssl->handler(c); //实际处理逻辑位于ngx_http_ssl_handshake_handler(gdb) sngx_http_ssl_handshake_handler (c=0x15da400) at src/http/ngx_http_request.c:782782 {(gdb) n805 if (hc->addr_conf->http2) { //配置http2后hc->addr_conf->http2标志位为1(gdb) n808 SSL_get0_alpn_selected(c->ssl->connection, &data, &len);//从ssl协议中取出alpn(gdb) n820 if (len == 2 && data[0] == ‘h’ && data[1] == ‘2’) { //如果为h2,说明客户端支持升级到http2协议(gdb) n821 ngx_http_v2_init(c->read);//开始进入http2的初始化阶段简单说就是通过ssl协议握手阶段获取一个alpn相关的配置,如果是h2,就进入http2的处理流程。我们通过wireshark抓包可以更直观的看出这个流程如上图,在ssl握手中的Client Hello 阶段有一个协议扩展alpnhttp2报文格式http2 以一个preface开头,接着是一个个的frame,其中每个frame都有一个header,如下:其中length代表frame内容的长度,type表明frame的类型,flag给frame做一些特殊的标记,sid代表的就是frame的id.其中 frame有如下10种类型#define NGX_HTTP_V2_DATA_FRAME 0x0 //body数据#define NGX_HTTP_V2_HEADERS_FRAME 0x1 //header数据#define NGX_HTTP_V2_PRIORITY_FRAME 0x2 //优先级设置#define NGX_HTTP_V2_RST_STREAM_FRAME 0x3 //重置一个stream#define NGX_HTTP_V2_SETTINGS_FRAME 0x4 //其他设置项,例如是否开启push,同时能够处理的stream数量等#define NGX_HTTP_V2_PUSH_PROMISE_FRAME 0x5 //push#define NGX_HTTP_V2_PING_FRAME 0x6 //ping#define NGX_HTTP_V2_GOAWAY_FRAME 0x7 //goaway.发送此frame后会重新建立连接#define NGX_HTTP_V2_WINDOW_UPDATE_FRAME 0x8 //窗口更新 流控使用#define NGX_HTTP_V2_CONTINUATION_FRAME 0x9 //当一个frame发送不完数据时,可以按continuation格式继续发送frame ID在客户端按奇数递增,例如1,3,5,偶数型id留给服务端推送push时使用,设置连接属性相关的frame id都为0flags有如下定义:#define NGX_HTTP_V2_NO_FLAG 0x00 //未设置#define NGX_HTTP_V2_ACK_FLAG 0x01 //ack flag#define NGX_HTTP_V2_END_STREAM_FLAG 0x01 //结束stream#define NGX_HTTP_V2_END_HEADERS_FLAG 0x04 //结束headers#define NGX_HTTP_V2_PADDED_FLAG 0x08 //填充flag#define NGX_HTTP_V2_PRIORITY_FLAG 0x20 //优先级设置flag如下是一个http头类型frame具体的内容格式:padded和priority由上文头部的flag决定是否有这两字段。接下来占8bit的flag决定header是否需要索引,如果需要,索引号是多少。huff(1)表明该字段是否使用了huffman编码。header_value_len(7)和header_value是具体头字段的value值如下是一个设置相关的frame如下是一个窗口更新的frame下边我们看一个具体的例子,来更直观的了解下。http2报文解析新版本的curl有一个–http2参数,可以直接指明使用http2进行通讯。我们将客户端命令修改如下:curl –http2 -k -I -L https://10.96.79.14:8443通过上边的gdb跟踪,我们看到http2初始化入口函数为ngx_http_v2_init,直接在此处打断点,继续跟踪代码.跟踪过程不再详细描述,当把报文读取进缓存之后,我们直接在gdb中bt查看调用路径,如下:#0 ngx_http_v2_state_preface (h2c=0x15a9310, pos=0x164b0b0 “PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n”, end=0x164b11e “”) at src/http/v2/ngx_http_v2.c:713#1 0x00000000004bca20 in ngx_http_v2_read_handler (rev=0x16141f0) at src/http/v2/ngx_http_v2.c:415#2 0x00000000004bcf8a in ngx_http_v2_init (rev=0x16141f0) at src/http/v2/ngx_http_v2.c:328#3 0x0000000000490a13 in ngx_http_ssl_handshake_handler (c=0x15da400) at src/http/ngx_http_request.c:821#4 0x000000000047de24 in ngx_ssl_handshake_handler (ev=0x16141f0) at src/event/ngx_event_openssl.c:1390#5 0x0000000000479637 in ngx_epoll_process_events (cycle=0x1597e30, timer=<optimized out>, flags=<optimized out>) at src/event/modules/ngx_epoll_module.c:902#6 0x000000000046f9db in ngx_process_events_and_timers (cycle=0x1597e30) at src/event/ngx_event.c:242#7 0x000000000047761c in ngx_worker_process_cycle (cycle=0x1597e30, data=<optimized out>) at src/os/unix/ngx_process_cycle.c:750#8 0x0000000000475c50 in ngx_spawn_process (cycle=0x1597e30, proc=0x477589 <ngx_worker_process_cycle>, data=0x0, name=0x684922 “worker process”, respawn=-3) at src/os/unix/ngx_process.c:199#9 0x00000000004769aa in ngx_start_worker_processes (cycle=0x1597e30, n=1, type=-3) at src/os/unix/ngx_process_cycle.c:359#10 0x0000000000477cb0 in ngx_master_process_cycle (cycle=0x1597e30) at src/os/unix/ngx_process_cycle.c:131#11 0x0000000000450ea4 in main (argc=<optimized out>, argv=<optimized out>) at src/core/nginx.c:382调用到ngx_http_v2_state_preface这个函数之后,开始处理http2请求,我们将请求内容打印出来看一下:(gdb) p end-pos$1 = 110(gdb) p pos@110$2 = “PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n\000\000\022\004\000\000\000\000\000\000\003\000\000\000d\000\004@\000\000\000\000\002\000\000\000\000\000\000\004\b\000\000\000\000\000?\377\000\001\000\000%\001\005\000\000\000\001B\004HEAD\204\207A\214\b\027}\305\335}p\265q\346\232gz\210%\266Pë\266\322\340S\003/*“nginx接下来开始处理http2请求,处理方法可以按上述方法继续跟踪,我们直接按http2协议将上述报文解析一下,如下所示:注意gdb打印出来的是八进制格式http push抓包注意上文nginx配置中配置了两条http2_push指令,即服务端会在请求index.html时主动将favicon.ico和nginx.png两个图片push下去。wireshark中抓包如下:服务端首先发送一个push_promise报文,报文中会包括push的文件路径和frame id.第二个和第三个红框即开始push具体的信息,frame id分别为2和4我们从浏览器端看一下push的请求:不主动push请求如下:浏览器必须首先将index.html加载之后才会知道接着去请求哪些资源,于是favicon.ico和nginx.png就会延迟加载。问题HTTP2如果在服务端动态索引header,会使http变成有状态的服务,集群之间如何解决header头缓存的问题?静态资源文件首次请求后会在浏览器端缓存,push如何保证只推送一次(即只有首次请求时才push)?参考资料1.https://www.nginx.com/blog/ht…2.https://httpwg.org/specs/rfc7540 ...

December 11, 2018 · 2 min · jiezi

webpack 入门与解析

每次学新东西总感觉自己是不是变笨了,看了几个博客,试着试着就跑不下去,无奈只有去看官方文档。 webpack是基于node的。先安装最新的node。1.初始化安装node后,新建一个目录,比如html5。cmd中切到当前文件夹。npm init -y这个命令会创建一个默认的package.json。它包含了项目的一些配置参数,通过它可以进行初始安装。详细参数:https://docs.npmjs.com/files/package.json。不要y参数的话,会在命令框中设置各项参数,但觉得没啥必要。2.安装webpacknpm install webpack --save-dev将webpack安装到当前目录。虽然npm install webpack -g 可以讲webpack安装到全局,但是容易出现一些模块找不到的错误,所以最好还是安装到当前目录下。3.目录结构webpack是一款模块加载各种资源并打包的工具。所以先建一个如下的目录结构:app包含的开发中的js文件,一个组件,一个入口。build中就是用来存放打包之后的文件的。webpack.config.js 顾名思义用来配置webpack的。package.json就不用说了。component.jsexport default function () {``var element = document.createElement(``'h1'``);``element.innerHTML = 'Hello world'``;``return element;``}component.js 是输出一个内容为h1元素。export default 是ES6语法,表示指定默认输出。import的时候不用带大括号。index.jsimport component from './component'``;``document.body.appendChild(component());index.js 的作用就是引用Component模块,并在页面上输出一个h1元素。但完成这个还需要一个插件,因为目前我们还没有index.html文件。npm install html-webpack-plugin --save-devhtml-webpack-plugin的用来生成html,将其也安装到开发目录下面。4.设置 webpack 配置文件我们需要通过webpack.config.js文件告诉webpack如何开始。配置文件至少需要一个入口和一个输出。多个页面就需要多个入口。node的path模块const path = require(``'path'``);``const HtmlWebpackPlugin = require(``'html-webpack-plugin'``);``const PATHS = {``app: path.join(__dirname, 'app'``),``build: path.join(__dirname, 'build'``),``};``module.exports = {``entry: {``app: PATHS.app,``},``output: {``path: PATHS.build,``filename: '[name].js'``,``},``plugins: [``new HtmlWebpackPlugin({``title: 'Webpack demo'``,``}),``],``};第一次看到这个配置文件是有点懵,主要是exports,分三个部分,一个入口,一个输出,一个插件。入口指向了app文件夹。默认会把包含"index.js"的文件作为入口。输出指定了build地址和一个文件名;[name]这儿表示占位符,可以看成webpack提供的一个变量。这个具体后面再看。而HtmlWebpackPlugin会生成一个默认的html文件。5.打包有了以上准备,直接输入 webpack 就能运行了。这个输出包含了Hash(每次打包值都不同),Version,Time(耗时)。以及输出的文件信息。这时打开build文件夹,发现多了一个app.js和index.html文件,双击index.html:也可以修改下package.json?{``"name"``: "Html5"``,``"version"``: "1.0.0"``,``"description"``: ""``,``"main"``: "index.js"``,``"scripts"``: {``"build"``: "webpack"``},``"keywords"``: [],``"author"``: ""``,``"license"``: "ISC"``,``"devDependencies"``: {``"html-webpack-plugin"``: "^2.28.0"``,``"webpack"``: "^2.2.1"``}``}指定build。在cmd中执行npm run build 得到同样的结果出现helloword。再看下文件内容index.html:&lt;!DOCTYPE html&gt;``&lt;``html``&gt;``&lt;``head``&gt;``&lt;``meta charset``=``"UTF-8"``&gt;``&lt;``title``&gt;Webpack demo&lt;/``title``&gt;``&lt;/``head``&gt;``&lt;``body``&gt;``&lt;``script type``=``"text/javascript" src``=``"app.js"``&gt;&lt;/``script``&gt;&lt;/``body``&gt;``&lt;/``html``&gt;默认引用了app.js。6、解析app.js/******/ (``function``(modules) { // webpackBootstrap``/******/ // The module cache``/******/ var installedModules = {};``/******/ // The require function``/******/ function __webpack_require__(moduleId) {``/*****/ // Check if module is in cache``/******/ if``(installedModules[moduleId])``/******/ return installedModules[moduleId].exports;``/******/ // Create a new module (and put it into the cache)``/******/ var module = installedModules[moduleId] = {``/******/ i: moduleId,``/******/ l: false``,``/******/ exports: {}``/******/ };``/******/ // Execute the module function``/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);``/******/ // Flag the module as loaded``/******/ module.l = true``;``/******/ // Return the exports of the module``/******/ return module.exports;``/******/ }``/******/ // expose the modules object (__webpack_modules__)``/******/ __webpack_require__.m = modules;``/******/ // expose the module cache``/******/ __webpack_require__.c = installedModules;``/******/ // identity function for calling harmony imports with the correct context``/******/ __webpack_require__.i = function``(value) { return value; };``/******/ // define getter function for harmony exports``/******/ __webpack_require__.d = function``(exports, name, getter) {``/******/ if``(!__webpack_require__.o(exports, name)) {``/******/ Object.defineProperty(exports, name, {``/******/ configurable: false``,``/******/ enumerable: true``,``/******/ get: getter``/******/ });``/******/ }``/******/ };``/******/ // getDefaultExport function for compatibility with non-harmony modules``/******/ __webpack_require__.n = function``(module) {``/******/ var getter = module &amp;&amp; module.__esModule ?``/******/ function getDefault() { return module[``'default'``]; } :``/******/ function getModuleExports() { return module; };``/******/ __webpack_require__.d(getter, 'a'``, getter);``/******/ return getter;``/******/ };``/******/ // Object.prototype.hasOwnProperty.call``/******/ __webpack_require__.o = function``(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };``/******/ // __webpack_public_path__``/******/ __webpack_require__.p = ""``;``/******/ // Load entry module and return exports``/******/ return __webpack_require__(__webpack_require__.s = 1);``/******/ })``/************************************************************************/``/******/ ([``/* 0 */``/***/ (``function``(module, __webpack_exports__, __webpack_require__) {``"use strict"``;``/* harmony default export */ __webpack_exports__[``"a"``] = function () {``var element = document.createElement(``'h1'``);``element.innerHTML = 'Hello world'``;``return element;};/***/` `}),/* 1 //***/` `(function(module, __webpack_exports__, __webpack_require__) {“use strict”;Object.defineProperty(webpack_exports, "__esModule", { value:` `true` `});/ harmony import / var WEBPACK_IMPORTED_MODULE_0__component = webpack_require(0);document.body.appendChild(__webpack_require__.i(__WEBPACK_IMPORTED_MODULE_0__component__[“a” / default /])());// })/******/` `]);`而app.js内容比较多了。整体是一个匿名函数。`(function(module) {})([(function` `(){}),` `function() {}])app文件夹中的两个js文件成了这儿的两个模块。函数最开始是从__webpack_require__开始return webpack_require(webpack_require.s = 1);这里指定从模块1执行(赋值语句的返回值为其值)。而模块1的调用是通过__webpack_require__的这句执行的。&lt;u&gt;复制代码&lt;/u&gt; 代码如下:modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);通过call调用模块的主要作用是为了把参数传过去。(function(module, webpack_exports, webpack_require) {"use strict";Object.defineProperty(__webpack_exports__,` `"__esModule", { value: true });/* harmony import */` `var` `__WEBPACK_IMPORTED_MODULE_0__component__ = __webpack_require__(0);document.body.appendChild(webpack_require.i(WEBPACK_IMPORTED_MODULE_0__component["a"` `/* default */])());/***/` `})`webpack_require 每加载一个模块都会先去模块缓存中找,没有就新建一个module对象:`var` `module = installedModules[moduleId] = {i: moduleId,l:` `false,exports: {}};模块1中加载了模块0,var WEBPACK_IMPORTED_MODULE_0__component = webpack_require(0);WEBPACK_IMPORTED_MODULE_0__component 返回的是这个模块0的exports部分。而之前Component.js的默认方法定义成了webpack_exports["a"] = function () {var` `element = document.createElement(‘h1’);element.innerHTML = ‘Hello world’;return element;}`所以再模块1的定义通过"a“来获取这个方法:&lt;u&gt;复制代码&lt;/u&gt; 代码如下:document.body.appendChild(__webpack_require__.i(__WEBPACK_IMPORTED_MODULE_0__component__["a" /* default */])());这样就完整了,但这里使用了__webpack_require__.i 将原值返回。`/******/`&nbsp; `// identity function for calling harmony imports with the correct context/****/&nbsp; webpack_require.i = function``(value) { return value; };`不太明白这个i函数有什么作用。这个注释也不太明白,路过的大神希望可以指点下。小结:webpack通过一个立即执行的匿名函数将各个开发模块作为参数初始化,每个js文件(module)对应一个编号,每个js中export的方法或者对象有各自指定的关键字。通过这种方式将所有的模块和接口方法管理起来。然后先加载最后的一个模块(应该是引用别的模块的模块),这样进而去触发别的模块的加载,使整个js运行起来。到这基本了解了webpack的功能和部分原理,但略显复杂,且没有感受到有多大的好处。继续探索。 ...

December 9, 2018 · 2 min · jiezi

十大热门的JavaScript框架和库

JavaScript 框架和库可以说是开源项目中最庞大也是最累的类目了,目前在github 上这一类的项目是最多的,并且几乎每隔一段时间就会出现一个新的项目席卷网络社区,虽然这样推动了创新的发展,但不得不说苦了前端的开发者们。因此本文罗列出了一些优秀的 Javascript 框架和库的特及其在 github 上的 star 数,旨在为各位开发者提供一些参考。1、ReactJS(Star: 59989,Fork: 10992)主页:了解更多React.js(React)是一个用来构建用户界面的 JavaScript 库,主要用于构建UI,很多人认为 React 是 MVC 中的 V(视图)。React 起源于 Facebook 的内部项目,用来架设 Instagram 的网站,并于 2013 年 5 月开源。React 拥有较高的性能,代码逻辑非常简单,越来越多的人已开始关注和使用它。React 特点: 1.声明式设计−React采用声明范式,可以轻松描述应用。 2.高效−React通过对DOM的模拟,最大限度地减少与DOM的交互。 3.灵活−React可以与已知的库或框架很好地配合。 4.JSX− JSX 是 JavaScript 语法的扩展。React 开发不一定使用 JSX ,但我们建议使用它。 5.组件− 通过 React 构建组件,使得代码更加容易得到复用,能够很好的应用在大项目的开发中。 6.单向响应的数据流− React 实现了单向响应的数据流,从而减少了重复代码,这也是它为什么比传统数据绑定更简单。2、AngularJS(Star: 54769,Fork: 27292)主页:https://angularjs.orgAngular JS (Angular.JS) 是一组用来开发 Web 页面的框架、模板以及数据绑定和丰富 UI 组件。它支持整个开发进程,提供 Web 应用的架构,无需进行手工 DOM 操作。 AngularJS 很小,只有 60K,兼容主流浏览器,与 jQuery 配合良好。3、Vue.js(Star: 43608, Fork: 5493)https://cn.vuejs.org/Vue.js 是构建 Web 界面的 JavaScript 库,提供数据驱动的组件,还有简单灵活的 API,使得 MVVM 更简单。主要特性: ●可扩展的数据绑定 ●将普通的 JS 对象作为 model ●简洁明了的 API ●组件化 UI 构建 ●配合别的库使用4、jQuery(Star: 43432, Fork: 12117)主页:https://jquery.com/JQuery 是轻量级的js库(压缩后只有21k) ,它兼容CSS3,还兼容各种浏览器 (IE 6.0+, FF 1.5+, Safari 2.0+, Opera 9.0+)。jQuery使用户能更方便地处理HTML documents、events、实现动画效果,并且方便地为网站提供AJAX交互。jQuery还有一个比较大的优势是,它的文档说明很全,而且各种 应用也说得很详细,同时还有许多成熟的插件可供选择。jQuery能够使用户的html页保持代码和html内容分离,也就是说,不用再在html里面插入一堆js来调用命令了,只需定义id即可。5、Meteor(Star: 36691,Fork: 4617)主页:http://www.meteor.comMeteor 是一组新的技术用于构建高质量的 Web 应用,提供很多现成的包,可直接在浏览器或者云平台中运行。6、Angular2(Star:20803,Fork:5367)主页:https://angular.ioAngular 是一款十分流行且好用的 Web 前端框架,目前由 Google 维护。这个条目收录的是 Angular 2 及其后面的版本。由于官方已将 Angular 2 和之前的版本Angular.js分开维护(两者的 GitHub 地址和项目主页皆不相同),所以就有了这个页面。7、Ember.js(Star: 17540,Fork: 3646)主页:http://emberjs.comEmber是一个雄心勃勃的Web应用程序,消除了样板,并提供了一个标准的应用程序架构的JavaScript框架。8、Polymer(Star:16979,Fork: 1699)主页:http://www.polymer-project.org在2013年的Google I/O大会上,Google发布了Polymer,它是一个使用Web组件构建Web应用的类库,同时也使用了为Web构建可重用组件的新的HTML 5标准。Polymer为大部分Web组件技术提供了polyfills功能,它能让开发者在所有的浏览器支持新特性前创建自己的可重用组件。此外,Polymer提供了一系列的部件的例子,其中包括天气、时钟、股票行情和线型图。Polymer中的polyfills为需要使用Web组件成功构建应用提供了多种Web技术,包括: ●HTML imports:种在其他HTML document中引入和重用HTML document的方法。 ●自定义元素:让开发者定义和使用自定义DOM元素。 ●Shadow DOM:在DOM中提供的封装。 ●模型驱动视图(Model Driven Views):提供象AngularJS的数据绑定。 ●Web动画:实现复杂动画的API。 ●Pointer事件:对鼠标触摸和手写笔事件的封装9、Zepto.js(Star: 12074,Fork: 3260)主页:https://facebook.github.io/react Zepto.js 是支持移动WebKit浏览器的JavaScript框架,具有与jQuery兼容的语法。2-5k的库,通过不错的API处理绝大多数的基本工作。10、Riot.js(Star: 11491,Fork: 902)主页:http://riotjs.comRiot.js是一个客户端模型-视图-呈现(MVP)框架并且它非常轻量级甚至小于1kb.尽管他的大小令人难以置信,所有它能构建的有如下:一个模板引擎,路由,甚至是库和一个严格的并具有组织的MVP模式。当模型数据变化时视图也会自动更新。当然除了以上提到的这些,还有很多优秀的 Javascript 框架和库,并且几乎每隔一段时间就会涌现一个新的产品。 ...

December 9, 2018 · 1 min · jiezi

服务器获取真实客户端 IP

服务器获取真实客户端 IP0x01 先查个问题测试环境微信支付通道提示网络环境未能通过安全验证,请稍后再试,出现这种情况一般首要 想到可能是双方网络交互中微信方验参与我们出现不一致,翻了下手册确定是这类问题开始排查环节可能获取真实IP方式错误getenv(‘HTTP_CLIENT_IP’)getenv(‘HTTP_X_FORWARDED_FOR’)getenv(‘REMOTE_ADDR’)filter_var($remote_ip, FILTER_VALIDATE_IP)已经依次获取并过滤固程序没有任何问题,往上发散是否反向代理经过反向代理后,由于在客户端和web服务器之间增加了中间层,因此web服务器无法直接拿到客户端的ip,只能通过$remote_addr变量拿到的将是反向代理服务器的ip地址,检查不存在此类问题,再往上,擅长网络工程的同学表示绝不认输可能NAT分配出口IP,或负载均衡服务分发出现异常先拿到我本地内网外网IP 方便之后问题排查# 本机IPifconfig | grep -A 1 “en” | grep broadcast | cut -d " " -f 2# 外网IPcurl –silent http://icanhazip.com检查与80端口建立连接目标都有谁 netstat -tn|grep 80|akw ‘{print $5}’|awk -F ‘{print $1}’ | grep [本地IP]这里出现问题,竟然没有我的IP,再以nginx $remote_addr拿到的IP作为参考,这是nginx最后一次握手的IP,$remote_addr = 10.168.0.0/16 段在nginx处打印$remote_addr,并在server_name添加当前机器ip,分别以负载均衡IP与本地IP做测试,最终确定问题出现在负载均衡服务器出现异常0x02 LNMP栈拿真实IPLNMP栈内PHP所有获得到的TCP操作信息都是由前面Nginx通过fastcgi传递给它的,就比如$_SERVER[‘REMOTE_ADDR’]由include fastcgi.conf;引进,其等于nginx的$remote_addrNginx中的几个变量:$remote_addr代表客户端的IP,但它的值不是由客户端提供的,而是服务端根据客户端的ip指定的,icanhazip的原理也是这样, 当你的浏览器访问某个网站时,假设中间没有任何代理,那么网站的web服务器就会把remote_addr设为你在公网暴露的IP,如果你用了某个代理,那么你的浏览器会先访问这个代理,然后再由这个代理转发到网站,这样web服务器就会把remote_addr设为这台代理机器的IP, 除非代理将你的IP附在请求header中一起转交给web服务器。$proxy_add_x_forwarded_for $proxy_add_x_forwarded_for变量包含客户端请求头中的"X-Forwarded-For",与$remote_addr两部分,他们之间用逗号分开。X-Forwarded-For(简称XFF),X-Forwarded-For 是一个 HTTP 扩展头部。RFC 2616 协议并没有对它的定义,它最开始是由 Squid 这个缓存代理软件引入,用来表示 HTTP 请求端真实 IP。如今它已经成为事实上的标准,被各大HTTP 代理、负载均衡等转发服务广泛使用,并被写入 RFC 7239(Forwarded HTTP Extension` 标准之中。$proxy_set_header已在排查问题中说明,可设置代理后 header proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme;X-Real-IP 一般比如X-Real-IP这一个自定义头部字段,通常被 HTTP 代理用来表示与它产生TCP 连接的设备 IP,这个设备可能是其他代理,也可能是真正的请求端,这个要看经过代理的层级次数或是是否始终将真实IP一路传下来。(牢记:任何客户端传上来的东西都是不可信的)当多层代理或使用CDN时,如果代理服务器不把用户的真实IP传递下去,那么服务器将永远不可能获取到用户的真实IP。0x03 用户的真实IP从何而来宽带供应商提供独立IP 比如家里电信宽带上网,电信给分配了公网ip,那么一个请求经过的ip路径如下: 192.168.0.1 (用户) –> 192.168.0.1/116.1.2.3 (路由器的局域网ip及路由器得到的电信公网ip)–> 119.110.0.0/16 (负载均衡服务器)–> 10.168.0.0/32 (业务处理服务器)这种情况下,119.147.19.234 会把得到的116.1.2.3附加到头信息中传给10.168.0.0/32,因此这种情况下,我们取得的用户ip则为:116.1.2.3。如果119.110.0.0/16没有把116.1.2.3附加到头信息中传给业务服务器,业务服务器就只能取上上一级ip地址宽带供应商不能提供独立IP宽带提供商没有足够的公网ip,分配的是个内网ip,比如长宽等小的isp。请求路径则可能如下: 192.168.0.1 (用户) –> 192.168.0.1/10.0.1.2(路由器的局域网ip及路由器得到的运营商内网ip)–> 211.162.78.1 (网络运营商长城宽带的公网ip) 119.110.0.0/16 (负载均衡服务器)–> 10.168.0.0/32 (业务处理服务器)这种情况下得到的用户ip,就是211.162.78.1。 这种情况下,就可能出现一个ip对应有数十上百个用户的情况了手机2g上网网络提供商没法直接提供ip给单个用户终端,以中国移动cmwap上网为例,因此请求路径可能为: 手机(手机上没法查看到ip)–> 10.0.0.172(cmwap代理服务器ip)–> 10.0.1.2(移动运营商内网ip)–> 202.96.75.1(移动运营商的公网ip)–> 119.110.0.0/16 (负载均衡服务器)–> 10.168.0.0/32 (业务处理服务器)这种情况下得到的用户ip,就是202.96.75.1。2008年的时候整个广东联通就三个手机上网的公网ip,因此这种情况下,同一ip出现数十万用户也是正常的。有几万或数十万员工的公司,这种也会出现来自同一ip的超多用户,可能达到几万人,但出口IP可能就那么几个。0x04 NAT [Network Address Translation]中文意思是网络地址转换,它允许一个整体机构以一个公用IP地址出现在Internet上。NAT在 OSI参考模型的网络层 (第3层), 它是一种把内部私有网络地址(IP地址)翻译成合法网络IP地址的技术。NAT可以让那些使用私有地址的内部网络连接到Internet或其它IP网络上。NAT路由器在将内部网络的数据包发送到公用网络时,在IP包的报头把私有地址转换成合法的IP地址。RFC1918 规定了三块专有的地址,作为私有的内部组网使用A类:10.0.0.0 — 10.255.255.255 10.0.0.0/8B类:172.16.0.0 — 172.31.255.255 172.16.0.0/12C类:192.168.0.0 — 192.168.255.255 192.168.0.0/16这三块私有地址本身是可路由的,只是公网上的路由器不会转发这三块私有地址的流量;当一个公司内部配置了这些私有地址后,内部的计算机在和外网通信时,公司的边界路由会通过NAT或者PAT技术,将内部的私有地址转换成外网IP,外部看到的源地址是公司边界路由转换过的公网IP地址,这在某种意义上也增加了内部网络的安全性<center><img src="//i.qh13.cn/t/1544243390591.png" width=“348”/></center>这个过程是通过NAT中的本地址与全局地址映射条目来实现的,所以事先要在NAT路由器上配置这样的映射条目。<center><img src="//i.qh13.cn/t/1544243579906.png" width=“250”/></center>通过这种方式一个公网 IP 底下可以发私有的 IP 地址。0x05 IPV6 来了?<center><img src="//i.qh13.cn/t/1544241538381.png" width=“557”/></center>写这篇文章的时候看到有个推送,表示阿里全面应用IPV6,这件事的意义还挺重大的<center><img src="//i.qh13.cn/t/1544241839285.png" width=“265”/></center>我们知道,一段 IPv4 标准的 IP 地址,一共由 4 X 8 = 32 位二进制数字组成,理论上存在 2^32 个 IP 地址。等于 4,294,967,296 , 42 亿多个 IPv4 的地址。<center><img src="//i.qh13.cn/t/1544242178427.png" width=“751”/></center>参考世界互联网用户统计报告,全球现在大概有4,208,571,287人在上网,也就是说已经快到ipv4地址设计的最大IP数了不过不用担心,前面提到的 NAT,让 IPv4 公网 IP 哪怕用完了也能凑合过。<center><img src="//i.qh13.cn/t/1544243741020.png" width=“540”/></center>到了 IPv6 ,相比 IPv4 最大的提升,就是位数大大增加,变成了 8 个 4 位的十六进制数字。也就是说有 2^128个 IPv6 地址。地球上的每粒沙子都分一个也管够<center><img src="//i.qh13.cn/t/1544244141371.png" width=“300”/></center>存储2^128字节理论上什么概念呢,在当今的量子水平下,假设计算设备能够操作在原子一级,每公斤质量可存储大约10的25次方bits,存储2的128次方的字节大约需要272 trillion = 2720000亿公斤。最后,周末愉快,北京联通已经支持ipv6了,我在望京测试,可以拿到 ipv6地址 ...

December 8, 2018 · 1 min · jiezi

Spring Cloud底层原理

毫无疑问,Spring Cloud 是目前微服务架构领域的翘楚,无数的书籍博客都在讲解这个技术。不过大多数讲解还停留在对 Spring Cloud 功能使用的层面,其底层的很多原理,很多人可能并不知晓。实际上,Spring Cloud 是一个全家桶式的技术栈,它包含了很多组件。本文先从最核心的几个组件,也就是 Eureka、Ribbon、Feign、Hystrix、Zuul 入手,来剖析其底层的工作原理。业务场景介绍先来给大家说一个业务场景,假设咱们现在开发一个电商网站,要实现支付订单的功能。流程如下:创建一个订单后,如果用户立刻支付了这个订单,我们需要将订单状态更新为“已支付”。扣减相应的商品库存。通知仓储中心,进行发货。给用户的这次购物增加相应的积分。针对上述流程,我们需要有订单服务、库存服务、仓储服务、积分服务。整个流程的大体思路如下:用户针对一个订单完成支付之后,就会去找订单服务,更新订单状态。订单服务调用库存服务,完成相应功能。订单服务调用仓储服务,完成相应功能。订单服务调用积分服务,完成相应功能。至此,整个支付订单的业务流程结束。下面这张图,清晰表明了各服务间的调用过程:好!有了业务场景之后,咱们就一起来看看 Spring Cloud 微服务架构中,这几个组件如何相互协作,各自发挥的作用以及其背后的原理。Spring Cloud 核心组件:Eureka咱们来考虑第一个问题:订单服务想要调用库存服务、仓储服务,或者积分服务,怎么调用?订单服务压根儿就不知道人家库存服务在哪台机器上啊!它就算想要发起一个请求,都不知道发送给谁,有心无力!这时候,就轮到 Spring Cloud Eureka 出场了。Eureka 是微服务架构中的注册中心,专门负责服务的注册与发现。咱们来看看下面的这张图,结合图来仔细剖析一下整个流程:如上图所示,库存服务、仓储服务、积分服务中都有一个 Eureka Client 组件,这个组件专门负责将这个服务的信息注册到 Eureka Server 中。说白了,就是告诉 Eureka Server,自己在哪台机器上,监听着哪个端口。而 Eureka Server 是一个注册中心,里面有一个注册表,保存了各服务所在的机器和端口号。订单服务里也有一个 Eureka Client 组件,这个 Eureka Client 组件会找 Eureka Server 问一下:库存服务在哪台机器啊?监听着哪个端口啊?仓储服务呢?积分服务呢?然后就可以把这些相关信息从 Eureka Server 的注册表中拉取到自己的本地缓存中来。这时如果订单服务想要调用库存服务,不就可以找自己本地的 Eureka Client 问一下库存服务在哪台机器?监听哪个端口吗?收到响应后,紧接着就可以发送一个请求过去,调用库存服务扣减库存的那个接口!同理,如果订单服务要调用仓储服务、积分服务,也是如法炮制。总结一下:Eureka Client:负责将这个服务的信息注册到 Eureka Server 中。Eureka Server:注册中心,里面有一个注册表,保存了各个服务所在的机器和端口号。Spring Cloud 核心组件:Feign现在订单服务确实知道库存服务、积分服务、仓库服务在哪里了,同时也监听着哪些端口号了。但是新问题又来了:难道订单服务要自己写一大堆代码,跟其他服务建立网络连接,然后构造一个复杂的请求,接着发送请求过去,最后对返回的响应结果再写一大堆代码来处理吗?这是上述流程翻译的代码片段,咱们一起来看看,体会一下这种绝望而无助的感受!!!友情提示,前方高能:看完上面那一大段代码,有没有感到后背发凉、一身冷汗?实际上你进行服务间调用时,如果每次都手写代码,代码量比上面那段要多至少几倍,所以这个事压根儿就不是地球人能干的。既然如此,那怎么办呢?别急,Feign 早已为我们提供好了优雅的解决方案。来看看如果用 Feign 的话,你的订单服务调用库存服务的代码会变成啥样?看完上面的代码什么感觉?是不是感觉整个世界都干净了,又找到了活下去的勇气!没有底层的建立连接、构造请求、解析响应的代码,直接就是用注解定义一个 Feign Client 接口,然后调用那个接口就可以了。人家 Feign Client 会在底层根据你的注解,跟你指定的服务建立连接、构造请求、发起请求、获取响应、解析响应,等等。这一系列脏活累活,人家 Feign 全给你干了。那么问题来了,Feign 是如何做到这么神奇的呢?很简单,Feign 的一个关键机制就是使用了动态代理。咱们一起来看看上面的图,结合图来分析:首先,如果你对某个接口定义了 @FeignClient 注解,Feign 就会针对这个接口创建一个动态代理。接着你要是调用那个接口,本质就是会调用 Feign 创建的动态代理,这是核心中的核心。Feign的动态代理会根据你在接口上的 @RequestMapping 等注解,来动态构造出你要请求的服务的地址。最后针对这个地址,发起请求、解析响应。Spring Cloud 核心组件:Ribbon说完了 Feign,还没完。现在新的问题又来了,如果人家库存服务部署在了 5 台机器上。如下所示:192.168.169:9000192.168.170:9000192.168.171:9000192.168.172:9000192.168.173:9000这下麻烦了!人家 Feign 怎么知道该请求哪台机器呢?这时 Spring Cloud Ribbon 就派上用场了。Ribbon 就是专门解决这个问题的。它的作用是负载均衡,会帮你在每次请求时选择一台机器,均匀的把请求分发到各个机器上。Ribbon 的负载均衡默认使用的最经典的 Round Robin 轮询算法。这是啥?简单来说,就是如果订单服务对库存服务发起 10 次请求,那就先让你请求第 1 台机器、然后是第 2 台机器、第 3 台机器、第 4 台机器、第 5 台机器,接着再来—个循环,第 1 台机器、第 2 台机器。。。以此类推。此外,Ribbon 是和 Feign 以及 Eureka 紧密协作,完成工作的,具体如下:首先 Ribbon 会从 Eureka Client 里获取到对应的服务注册表,也就知道了所有的服务都部署在了哪些机器上,在监听哪些端口号。然后 Ribbon 就可以使用默认的 Round Robin 算法,从中选择一台机器。Feign 就会针对这台机器,构造并发起请求。对上述整个过程,再来一张图,帮助大家更深刻的理解:Spring Cloud 核心组件:Hystrix在微服务架构里,一个系统会有很多的服务。以本文的业务场景为例:订单服务在一个业务流程里需要调用三个服务。现在假设订单服务自己最多只有 100 个线程可以处理请求,然后呢,积分服务不幸的挂了,每次订单服务调用积分服务的时候,都会卡住几秒钟,然后抛出—个超时异常。咱们一起来分析一下,这样会导致什么问题?如果系统处于高并发的场景下,大量请求涌过来的时候,订单服务的 100 个线程都会卡在请求积分服务这块,导致订单服务没有一个线程可以处理请求。然后就会导致别人请求订单服务的时候,发现订单服务也挂了,不响应任何请求了。上面这个,就是微服务架构中恐怖的服务雪崩问题,如下图所示:如上图,这么多服务互相调用,要是不做任何保护的话,某一个服务挂了,就会引起连锁反应,导致别的服务也挂。比如积分服务挂了,会导致订单服务的线程全部卡在请求积分服务这里,没有一个线程可以工作,瞬间导致订单服务也挂了,别人请求订单服务全部会卡住,无法响应。但是我们思考一下,就算积分服务挂了,订单服务也可以不用挂啊!为什么?我们结合业务来看:支付订单的时候,只要把库存扣减了,然后通知仓库发货就 OK 了。如果积分服务挂了,大不了等它恢复之后,慢慢人肉手工恢复数据!为啥一定要因为一个积分服务挂了,就直接导致订单服务也挂了呢?不可以接受!现在问题分析完了,如何解决?这时就轮到 Hystrix 闪亮登场了。Hystrix 是隔离、熔断以及降级的一个框架。啥意思呢?说白了,Hystrix 会搞很多个小小的线程池,比如订单服务请求库存服务是一个线程池,请求仓储服务是一个线程池,请求积分服务是一个线程池。每个线程池里的线程就仅仅用于请求那个服务。打个比方:现在很不幸,积分服务挂了,会咋样?当然会导致订单服务里那个用来调用积分服务的线程都卡死不能工作了啊!但由于订单服务调用库存服务、仓储服务的这两个线程池都是正常工作的,所以这两个服务不会受到任何影响。这个时候如果别人请求订单服务,订单服务还是可以正常调用库存服务扣减库存,调用仓储服务通知发货。只不过调用积分服务的时候,每次都会报错。但是如果积分服务都挂了,每次调用都要去卡住几秒钟干啥呢?有意义吗?当然没有!所以我们直接对积分服务熔断不就得了,比如在 5 分钟内请求积分服务直接就返回了,不要去走网络请求卡住几秒钟,这个过程,就是所谓的熔断!那人家又说,兄弟,积分服务挂了你就熔断,好歹你干点儿什么啊!别啥都不干就直接返回啊?没问题,咱们就来个降级:每次调用积分服务,你就在数据库里记录一条消息,说给某某用户增加了多少积分,因为积分服务挂了,导致没增加成功!这样等积分服务恢复了,你可以根据这些记录手工加一下积分。这个过程,就是所谓的降级。为帮助大家更直观的理解,接下来用一张图,梳理一下 Hystrix 隔离、熔断和降级的全流程:Spring Cloud 核心组件:Zuul说完了 Hystrix,接着给大家说说最后一个组件:Zuul,也就是微服务网关。这个组件是负责网络路由的。不懂网络路由?行,那我给你说说,如果没有 Zuul 的日常工作会怎样?假设你后台部署了几百个服务,现在有个前端兄弟,人家请求是直接从浏览器那儿发过来的。打个比方:人家要请求一下库存服务,你难道还让人家记着这服务的名字叫做 inventory-service?部署在 5 台机器上?就算人家肯记住这一个,你后台可有几百个服务的名称和地址呢?难不成人家请求一个,就得记住一个?你要这样玩儿,那真是友谊的小船,说翻就翻!上面这种情况,压根儿是不现实的。所以一般微服务架构中都必然会设计一个网关在里面。像 Android、iOS、PC 前端、微信小程序、H5 等等,不用去关心后端有几百个服务,就知道有一个网关,所有请求都往网关走,网关会根据请求中的一些特征,将请求转发给后端的各个服务。而且有一个网关之后,还有很多好处,比如可以做统一的降级、限流、认证授权、安全,等等。如果想免费学习Java工程化、高性能及分布式、深入浅出。微服务、Spring,MyBatis,Netty源码分析的朋友可以加我的Java进阶群:478030634,群里有阿里大牛直播讲解技术,以及Java大型互联网技术的视频免费分享给大家。总结最后再来总结一下,上述几个 Spring Cloud 核心组件,在微服务架构中,分别扮演的角色:Eureka:各个服务启动时,Eureka Client 都会将服务注册到 Eureka Server,并且 Eureka Client 还可以反过来从 Eureka Server 拉取注册表,从而知道其他服务在哪里。Ribbon:服务间发起请求的时候,基于 Ribbon 做负载均衡,从一个服务的多台机器中选择一台。Feign:基于 Feign 的动态代理机制,根据注解和选择的机器,拼接请求 URL 地址,发起请求。Hystrix:发起请求是通过 Hystrix 的线程池来走的,不同的服务走不同的线程池,实现了不同服务调用的隔离,避免了服务雪崩的问题。Zuul:如果前端、移动端要调用后端系统,统一从 Zuul 网关进入,由 Zuul 网关转发请求给对应的服务。以上就是我们通过一个电商业务场景,阐述了 Spring Cloud 微服务架构几个核心组件的底层原理。文字总结还不够直观?没问题!我们将 Spring Cloud 的 5 个核心组件通过一张图串联起来,再来直观的感受一下其底层的架构原理: ...

November 22, 2018 · 1 min · jiezi

解决跨域问题,实例调用百度地图

1.什么是跨域?浏览器对于javascript的同源策略的限制,例如a.com下面的js不能调用b.com中的js,对象或数据(因为a.com和b.com是不同域),所以跨域就出现了。同域的概念又是什么呢?所谓的同源是指,域名、协议、端口均为相同。2.如何解决跨域?JSONP:JSONP 是一种非官方的跨域数据交互协议。JSONP 本质上是利用 <script><img><iframe>等标签不受同源策略限制,可以从不同域加载并执行资源的特性,来实现数据跨域传输。JSONP由两部分组成:回调函数和数据。回调函数是当响应到来时应该在页面中调用的函数,而数据就是传入回调函数中的JSON数据。<br/>这种方式非常好用,但是有一个缺陷,只能实现get请求。设置代理:可以在服务器搭建nginx代理转发,或者由后台去调用之后把结果返回给前端,后台做一下中转。还可以搭建node服务器,用node进行代理转发。请求的后台设置允许跨域:header(‘Access-Control-Allow-Origin:*’);//允许所有来源访问header(‘Access-Control-Allow-Method:POST,GET’);//允许访问的方式iframe:所以跨域通信其实很简单,在iframe和主页里都不断地检测hashtag有没有变化,一旦有变化,就做出相应的改变。setInterval(function() { var hashVal = window.location.hash.substr(1); document.body.style.backgroundColor = hashVal;}, 1000); 这么做的问题就是,需要不断地去检测hashtag是否改变,效率有点低,如果能通过原生的监听来实现,就会更加高效和优雅。这里就涉及到另一个iframe特性:可以设置其他iframe的大小,即使是不同域的。而页面的resize事件是可以监听的,所以就有了下面这个模型。<br/>主页面先把消息附加到hashtag,然后改变一个隐藏的(或者页面外的)iframe的size。这个iframe会监听resize事件,同时捕获到hashtag。捕获到hashtag后(也就是所需的数据),再对hashtag做进一步的处理。处理完后把数据传到主页内的一个iframe,或者直接操作该iframe。这样就比较优雅地完成了跨域操作。3.实例最近在做一个涉及到地图的项目,使用的是百度地图API,就出现了跨域的问题。http://api.map.baidu.com/geoc… 这个api的作用是获取周边地理信息,在调用的时候产生了跨域问题后面采用了JSONP的方式解决$.ajax({ url: ‘http://api.map.baidu.com/geocoder/v2/?address=成都&output=json&ak=sn4yosvUfbGYsdffew3wq23114’, type: ‘GET’, async:false,//设置同步。ajax默认异步 dataType: ‘jsonp’, jsonp:‘callback’,//传递给请求处理程序或页面的,用以获得jsonp回调函数名的参数名(默认为:callback) jsonpCallback:“callback”,//自定义的jsonp回调函数名称,默认为jQuery自动生成的随机函数名 timeout: 5000, contentType: ‘application/json; charset=utf-8’, success: function (result){ console.log(result); }})很多时候我们都会碰到跨域问题,但也有很多方法来解决跨域问题,在解决跨域时,我们也要注意一下安全性问题

November 22, 2018 · 1 min · jiezi

分布式和事务你真的很熟吗?

本文注重实战或者实现,不涉及CAP,略提ACID。本文适合基础分布式程序员:1、本文会涉及集群中节点的failover和recover问题.2、本文会涉及事务及不透明事务的问题.3、本文会提到微博和tweeter,并引出一个大数据问题.由于分布式这个话题太大,事务这个话题也太大,我们从一个集群的一个小小节点开始谈起。集群中存活的节点与同步分布式系统中,如何判断一个节点(node)是否存活?kafka这样认为:1、此节点和zookeeper能喊话.(Keep sessions with zookeeper through heartbeats.)2、此节点如果是个从节点,必须能够尽可能忠实地反映主节点的数据变化。也就是说,必须能够在主节点写了新数据后,及时复制这些变化的数据,所谓及时,不能拉下太多哦.那么,符合上面两个条件的节点就可以认为是存活的,也可以认为是同步的(in-sync).关于第1点,大家对心跳都很熟悉,那么我们可以这样认为某个节点不能和zookeeper喊话了:zookeeper-node:var timer = new timer().setInterval(10sec).onTime(slave-nodes,function(slave-nodes){ slave-nodes.forEach( node -> { boolean isAlive = node.heartbeatACK(15sec); if(!isAlive) { node.numNotAlive += 1; if(node.numNotAlive >= 3) { node.declareDeadOrFailed(); slave-nodes.remove(node); //回调也可 leader-node-app.notifyNodeDeadOrFailed(node) } }else node.numNotAlive = 0; }); }); timer.run(); //你可以回调也可以像下面这样简单的计时判断 leader-node-app: var timer = new timer() .setInterval(10sec).onTime(slave-nodes,function(slave-nodes){slave-nodes.forEach(node -> { if(node.isDeadOrFailed) { //node不能和zookeeper喊话了 }});});timer.run();关于第二点,要稍微复杂点了,怎么搞呢?来这么分析:1:数据 messages.:2:操作 op-log.3:偏移 position/offset.// 1. 先考虑messages// 2. 再考虑log的postion或者offset// 3. 考虑msg和off都记录在同源数据库或者存储设备上.(database or storage-device.) var timer = new timer().setInterval(10sec).onTime(slave-nodes,function(nodes){var core-of-cpu = 8;//嫌慢就并发呗 mod hash go!nodes.groupParallel(core-of-cpu).forEach(node -> { boolean nodeSucked = false; if(node.ackTimeDiff > 30sec) { //30秒内没有回复,node卡住了 nodeSucked = true; } if(node.logOffsetDiff > 100) { //node复制跟不上了,差距超过100条数据 nodeSucked = true; } if(nodeSucked) { //总之node“死”掉了,其实到底死没死,谁知道呢?network-error在分布式系统中或者节点失败这个事情是正常现象. node.declareDeadOrFailed(); //不和你玩啦,集群不要你了 nodes.remove(node); //该怎么处理呢,抛个事件吧. fire-event-NodeDeadOrFailed(node); }});});timer.run();上面的节点的状态管理一般由zookeeper来做,leader或者master节点也会维护那么点状态。那么应用中的leader或者master节点,只需要从zookeeper拉状态就可以,同时,上面的实现是不是一定最佳呢?不是的,而且多数操作可以合起来,但为了描述节点是否存活这个事儿,咱们这么写没啥问题。节点死掉、失败、不同步了,咋处理呢?好嘛,终于说到failover和recover了,那failover比较简单,因为还有其它的slave节点在,不影响数据读取。1:同时多个slave节点失败了?没有100%的可用性.数据中心和机房瘫痪、网络电缆切断、hacker入侵删了你的根,总之你rp爆表了.2:如果主节点失败了,那master-master不行嘛?keep-alived或者LVS或者你自己写failover吧.高可用架构(HA)又是个大件儿了,此文不展开了。我们来关注下recover方面的东西,这里把视野打开点,不仅关注slave节点重启后追log来同步数据,我们看下在实际应用中,数据请求(包括读、写、更新)失败怎么办?大家可能都会说,重试(retry)呗、重放(replay)呗或者干脆不管了呗!行,都行,这些都是策略,但具体怎么个搞法,你真的清楚了?一个bigdata问题我们先摆个探讨的背景:问题:消息流,比如微博的微博(真绕),源源不断地流进我们的应用中,要处理这些消息,有个需求是这样的:Reach is the number of unique people exposed to a URL on Twitter.那么,统计一下3小时内的本条微博(url)的reach总数。怎么解决呢?把某时间段内转发过某条微博(url)的人拉出来,把这些人的粉丝拉出来,去掉重复的人,然后求总数,就是要求的reach.为了简单,我们忽略掉日期,先看看这个方法行不行:/** ———————————* 1. 求出转发微博(url)的大V. * __________________________________/方法 :getUrlToTweetersMap(String url_id)SQL : / 数据库A,表url_user存储了转发某url的user /SELECT url_user.user_id as tweeter_idFROM url_userWHERE url_user.url_id = ${url_id} 返回 :[user_1,…,user_m]./* ———————————* 2. 求出大V的粉丝 * __________________________________/方法 : getFollowers(String tweeter_id);SQL : / 数据库B /SELECT users.id as user_idFROM usersWHERE users.followee_id = ${tweeter_id}返回:tweeter的粉丝./* ———————————* 3. 求出Reach* __________________________________/var url = queryArgs.getUrl();var tweeters = getUrlToTweetersMap();var result = new HashMap<String,Integer>();tweeters.forEach(t -> {// 你可以批量in + 并发读来优化下面方法的性能var followers = getFollowers(t.tweeter_id);followers.forEach(f -> { //hash去重 result.put(f.user_id,1);});});//Reachreturn result.size(); 顶呱呱,无论如何,求出了Reach啊!其实这又引出了一个很重要的问题,也是很多大谈框架、设计、模式却往往忽视的问题:性能和数据库建模的关系。1:数据量有多大?不知道读者有木有对这个问题的数据库I/O有点想法,或者虎躯一震呢?Computing reach is too intense for a single machine – it can require thousands of database calls and tens of millions of tuples.在上面的数据库设计中避免了JOIN,为了提高求大V粉丝的性能,可以将一批大V作为batch/bulk,然后多个batch并发读,誓死搞死数据库。这里将微博到转发者表所在的库,与粉丝库分离,如果数据更大怎么办?库再分表…OK,假设你已经非常熟悉传统关系型数据库的分库分表及数据路由(读路径的聚合、写路径的分发)、或者你对于sharding技术也很熟悉、或者你良好的结合了HBase的横向扩展能力并有一致性策略来解决其二级索引问题.总之,存储和读取的问题假设你已经解决了,那么分布式计算呢?2:微博这种应用,人与人之间的关系成图状(网),你怎么建模存储?而不仅仅对应这个问题,比如:某人的好友的好友可能和某人有几分相熟?看看用storm怎么来解决分布式计算,并提供流式计算的能力: // url到大V -> 数据库1 TridentState urlToTweeters = topology.newStaticState(getUrlToTweetersState()); // 大V到粉丝 -> 数据库2 TridentState tweetersToFollowers =topology.newStaticState(getTweeterToFollowersState());topology.newDRPCStream(“reach”).stateQuery(urlToTweeters, new Fields(“args”), new MapGet(), new Fields(“tweeters”)).each(new Fields(“tweeters”), new ExpandList(), new Fields(“tweeter”)).shuffle() / 大V的粉丝很多,所以需要分布式处理*/.stateQuery(tweetersToFollowers, new Fields(“tweeter”), new MapGet(), new Fields(“followers”)).parallelismHint(200) /* 粉丝很多,所以需要高并发 / .each(new Fields(“followers”), new ExpandList(), new Fields(“follower”)).groupBy(new Fields(“follower”)).aggregate(new One(), new Fields(“one”)) / 去重 /.parallelismHint(20).aggregate(new Count(), new Fields(“reach”)); / 计算reach数 */最多处理一次(At most once)回到主题,引出上面的例子,一是为了引出一个有关分布式(存储+计算)的问题,二是透漏这么点意思:码农,就应该关注设计和实现的东西,比如Jay Kreps是如何发明Kafka这个轮子的 : ]如果你还是码农级别,咱来务点实吧,前面我们说到recover,节点恢复的问题,那么我们恢复几个东西?基本的:1、节点状态2、节点数据本篇从数据上来讨论下这个问题,为使问题再简单点,我们考虑写数据的场景,如果我们用write-ahead-log的方式来保证数据复制和一致性,那么我们会怎么处理一致性问题呢?1:主节点有新数据写入.2:从节点追log,准备复制这批新数据。从节点做两件事:(1). 把数据的id偏移写入log;(2). 正要处理数据本身,从节点挂了。那么根据上文的节点存活条件,这个从节点挂了这件事被探测到了,从节点由维护人员手动或者其自己恢复了,那么在加入集群和小伙伴们继续玩耍之前,它要同步自己的状态和数据。问题来了:如果根据log内的数据偏移来同步数据,那么,因为这个节点在处理数据之前就把偏移写好了,可是那批数据lost-datas没有得到处理,如果追log之后的数据来同步,那么那批数据lost-datas就丢了。在这种情况下,就叫作数据最多处理一次,也就是说数据会丢失。最少处理一次(At least once)好吧,丢失数据不能容忍,那么我们换种方式来处理:1:主节点有新数据写入.2:从节点追log,准备复制这批新数据。从节点做两件事:(1). 先处理数据;(2). 正要把数据的id偏移写入log,从节点挂了。问题又来了:如果从节点追log来同步数据,那么因为那批数据duplicated-datas被处理过了,而数据偏移没有反映到log中,如果这样追,会导致这批数据重复。这种场景,从语义上来讲,就是数据最少处理一次,意味着数据处理会重复。仅处理一次(Exactly once)Transaction好吧,数据重复也不能容忍?要求挺高啊。大家都追求的强一致性保证(这里是最终一致性),怎么来搞呢?换句话说,在更新数据的时候,事务能力如何保障呢?假设一批数据如下:// 新到数据{transactionId:4urlId:99reach:5}现在要更新这批数据到库里或者log里,那么原来的情况是:// 老数据{transactionId:3urlId:99reach:3}如果说可以保证如下三点:1、事务ID的生成是强有序的.(隔离性,串行)2、同一个事务ID对应的一批数据相同.(幂等性,多次操作一个结果)3、单条数据会且仅会出现在某批数据中.(一致性,无遗漏无重复)那么,放心大胆的更新好了:// 更新后数据{ transactionId:4urlId:99//3 + 5 = 8reach:8}注意到这个更新是ID偏移和数据一起更新的,那么这个操作靠什么来保证:原子性。 你的数据库不提供原子性?后文略有提及。这里是更新成功了。如果更新的时候,节点挂了,那么库里或者log里的id偏移不写,数据也不处理,等节点恢复,就可以放心去同步,然后加入集群玩耍了。所以说,要保证数据仅处理一次,还是挺困难的吧?上面的保障“仅处理一次”这个语义的实现有什么问题呢?性能问题。这里已经使用了batch策略来减少到库或磁盘的Round-Trip Time,那么这里的性能问题是什么呢?考虑一下,采用master-master架构来保证主节点的可用性,但是一个主节点失败了,到另一个主节点主持工作,是需要时间的。假设从节点正在同步,啪!主节点挂了!因为要保证仅处理一次的语义,所以原子性发挥作用,失败,回滚,然后从主节点拉失败的数据(你不能就近更新,因为这批数据可能已经变化了,或者你根本没缓存本批数据),结果是什么呢?老主节点挂了, 新的主节点还没启动,所以这次事务就卡在这里,直到数据同步的源——主节点可以响应请求。如果不考虑性能,就此作罢,这也不是什么大事。你似乎意犹未尽?来吧,看看“银弹”是什么?Opaque-Transaction现在,我们来追求这样一种效果:某条数据在一批数据中(这批数据对应着一个事务),很可能会失败,但是它会在另一批数据中成功。 换句话说,一批数据的事务ID一定相同。来看看例子吧,老数据不变,只是多了个字段:prevReach。// 老数据{transactionId:3urlId:99//注意这里多了个字段,表示之前的reach的值prevReach:2reach:3}// 新到数据{transactionId:4urlId:99reach:5}这种情况,新事务的ID更大、更靠后,表明新事务可以执行,还等什么,直接更新,更新后数据如下:// 新到数据{transactionId:4urlId:99//注意这里更新为之前的值prevReach:3//3 + 5 = 8reach:8}现在来看下另外的情况:// 老数据{transactionId:3urlId:99prevReach:2reach:3}// 新到数据{//注意事务ID为3,和老数据中的事务ID相同transactionId:3urlId:99reach:5}这种情况怎么处理?是跳过吗?因为新数据的事务ID和库里或者log里的事务ID相同,按事务要求这次数据应该已经处理过了,跳过?不,这种事不能靠猜的,想想我们有的几个性质,其中关键一点就是:给定一批数据,它们所属的事务ID相同。仔细体会下,上面那句话和下面这句话的差别: 给定一个事务ID,任何时候,其所关联的那批数据相同。我们应该这么做,考虑到新到数据的事务ID和存储中的事务ID一致,所以这批数据可能被分别或者异步处理了,但是,这批数据对应的事务ID永远是同一个,那么,即使这批数据中的A部分先处理了,由于大家都是一个事务ID,那么A部分的前值是可靠的。所以,我们将依靠prevReach而不是Reach的值来更新:// 更新后数据{transactionId:3urlId:99//这个值不变prevReach:2//2 + 5 = 7reach:7}你发现了什么呢?不同的事务ID,导致了不同的值:1:当事务ID为4,大于存储中的事务ID3,Reach更新为3+5 = 8.2:当事务ID为3,等于存储中的事务ID3,Reach更新为2+5 = 7.这就是Opaque Transaction.这种事务能力是最强的了,可以保证事务异步提交。所以不用担心被卡住了,如果说集群中:Transaction:数据是分批处理的,每个事务ID对应一批确定、相同的数据.保证事务ID的产生是强有序的.保证分批的数据不重复、不遗漏.如果事务失败,数据源丢失,那么后续事务就卡住直到数据源恢复.Opaque-Transaction:数据是分批处理的,每批数据有确定而唯一的事务ID.保证事务ID的产生是强有序的.保证分批的数据不重复、不遗漏.如果事务失败,数据源丢失,不影响后续事务,除非后续事务的数据源也丢了.其实这个全局ID的设计也是门艺术:冗余关联表的ID,以减少join,做到O(1)取ID.冗余日期(long型)字段,以避免order by.冗余过滤字段,以避免无二级索引(HBase)的尴尬.存储mod-hash的值,以方便分库、分表后,应用层的数据路由书写.这个内容也太多,话题也太大,就不在此展开了。你现在知道twitter的snowflake生成全局唯一且有序的ID的重要性了。两阶段提交现在用zookeeper来做两阶段提交已经是入门级技术,所以也不展开了。如果你的数据库不支持原子操作,那么考虑两阶段提交吧。如果想免费学习Java工程化、高性能及分布式、深入浅出。微服务、Spring,MyBatis,Netty源码分析的朋友可以加我的Java进阶群:478030634,群里有阿里大牛直播讲解技术,以及Java大型互联网技术的视频免费分享给大家。结语To be continued. ...

November 21, 2018 · 2 min · jiezi

从程序员的角度深入理解MySQL

数据库基本原理第一,数据库的组成:存储 + 实例不必多说,数据当然需要存储;存储了还不够,显然需要提供程序对存储的操作进行封装,对外提供增删改查的API,即实例。一个存储,可以对应多个实例,这将提高这个存储的负载能力以及高可用;多个存储可以分布在不同的机房、地域,将实现容灾。第二,按Block or Page读取数据用大腿想也知道,数据库不可能按行读取数据(Why? ? ^_^)。实质上,数据库,如Oracle/MySQL,都是基于固定大小(比如16K)的物理块(Block or Page,我这里就不区分统一称为Block)来实现调度和管理的。要知道Block是数据库的概念,如何对应到文件系统呢?显然需要指出“这个Block的地址在哪里”,当查找到地址后,读取固定大小的数据就相当于完成了Block的读取了。数据库很聪明的,它不会仅仅只读取需要读取的Block,它还会替我们把附近的Block块都读取加载至内存。实际上,这是为了减少IO次数,提高命中率。事实上,一个Block块的附近Block也是热点数据,这种处理方式很有必要!第三,磁盘IO是数据库的性能瓶颈毫无疑问,数据在磁盘上,少不了磁盘IO。什么磁头旋转,定位磁道,寻址的过程,就不说了,我们是程序员,也管不了这些。但是这个过程确实是非常耗时的,和内存读取不是一个数量级,所以后来出现了很多方式来减少IO,提升数据库性能。比如,增加内存,让数据库把数据更多的加载至内存。内存虽好,但也不能滥用,为什么这么说呢?假设数据库中有100G数据,如果都加载至内存,也就说数据库要管理100G磁盘数据+100G内存数据,你说累不累?(数据库要处理磁盘和内存的映射关系,数据的同步,还要对内存数据进行清理,如果涉及数据库事务,又是一系列复杂操作……)不过这里需要指出的是,为了加快内存查找速度,数据库一般对内存进行HASH存放。比如,利用索引,索引相比内存,是一个性价比非常高的东西,后文详细介绍MySQL的索引原理。比如,利用性能更好的磁盘…(和咱们就没关系呢)第四,提出一些问题思考下:为什么我们说利用delete删除一个表的数据较trancate一个表要慢?【一个按行查找删除,多费劲;一个基于Block的体系结构删除】为什么我们说要小表驱动大表?【小表驱动大表会快?什么鬼?MN和NM不是一样的么?有鬼的地方,就有索引!】探索MySQL索引背后的原理对于绝大数的应用系统,读写比例在10:1,甚至100:1,而且insert/update很难出现性能问题,遇到最多的,最棘手的就是select了,select优化是重中之重,显然少不了索引!说起MySQL的索引,我们会冒出很多这些东西:BTree索引/B+Tree索引/Hash索引/聚集索引/非聚集索引…这么多,晕头!索引到底是什么,想解决什么问题?老生常谈了,官网说MySQL索引是一种数据结构,索引的目的就是为了提高查询效率。说白了,不使用索引的话,磁盘IO次数比较多!要想减少磁盘IO次数,怎么办?我们想通过不断缩小想要获取的数据的范围来筛选出最终想要的结果,把每次查找数据的磁盘IO次数控制在一个很小的数量级,最好是常数数量级。为了应对上述问题,B+Tree索引出来了!Hello,B+Tree在MySQL中,不同存储引擎对索引的实现方式是不同的,这里将重点分析MyISAM和Innodb。我们知道对于MyISAM引擎而言,数据文件和索引文件是分离的。从图中也可以看出,通过索引查找到后,就得到了数据的物理地址,然后根据地址定位数据文件中的记录即可。这种方式也叫"非聚集索引"。而对于Innodb引擎而言,数据文件本身是索引文件!通俗点说,叶子节点上,MyISAM存储的是记录的物理地址,而Innodb上存储的是数据内容,这种方式即"聚集索引"。另外一点需要注意的是,对于Innodb而言,主键索引中叶子节点存储的是数据内容,而普通索引的叶子节点中存储的是主键值!也就是说,对于Innodb的普通索引字段查找,先通过普通索引的B+Tree查找到主键后,然后通过主键索引的B+Tree进行查找。从这里你可以看出,对于Innodb而言,主键的建立非常重要!而对于MyISAM而言,主键索引和普通索引仅仅的区别在于主键只需要查找到一条记录即可停止,而普通索引允许重复,找到一条记录后需要继续查找,在结构上没有区别,如上图所示。想利用索引,就得“干净”什么叫“干净”?就是不要让索引参与计算!比如在索引上应用函数,很可能导致索引失效。为什么呢?其实不用想,B+Tree上存储的是数据,要比较的话,需要把所有的数据都应用上函数,显然成本太大。想建立索引,看看区分度索引虽然物美价廉,但是也别乱来。count(distinct col) / count(*)可以算一下col的区分度,显然对于主键而言,就是1。区分度太低的话,可以考虑下,是否还有必要建立索引呢?Hash索引这里并不是要深入分析Hash索引,而是要说明一下Hash的思想真是无处不在!在MySQL的Memory存储引擎中,存在hash函数,给一个key,通过hash函数进行计算得到地址,所以通常情况下,hash索引查找,会非常快,O(1)的速度。但是也存在hash冲突,和HashMap一样,通过单链表的形式解决。思考下,hash索引是否支持范围查询呢?显然是不支持的,它只能给一个KEY去查找。就如同HashMap一样,查找key包含"zhangfengzhe"的,会很快么?在此我向大家推荐一个架构学习交流群。交流学习群号:478030634 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多SQL优化神器:explainSQL优化的场景很多,网上的技巧也很多,完全记不住!要想彻底解决这个问题,我想只有把索引背后的数据结构和原理做适当的理解,遇到书写SQL或者SQL慢查询的时候,我们有基础去分析,再利用好explain工具去验证,就应该问题不大呢。explain查询的结果,可以告诉你哪些索引正在被使用,表是如何被扫描的等等。这里我将演示个Demo。数据表student:注意复合索引(age,address)符合最左前缀匹配复合索引失效OK,到这里,准备结束了,查询容易,优化不易,且写且珍惜!

November 16, 2018 · 1 min · jiezi

【Nginx源码分析】Nginx的内存管理

施洪宝一. 概述应用程序的内存可以简单分为堆内存,栈内存。对于栈内存而言,在函数编译时,编译器会插入移动栈当前指针位置的代码,实现栈空间的自管理。而对于堆内存,通常需要程序员进行管理。我们通常说的内存管理亦是只堆空间内存管理。对于内存,我们的使用可以简化为3步,申请内存、使用内存、释放内存。申请内存,使用内存通常需要程序员显示操作,释放内存却并不一定需要程序员显示操作,目前很多的高级语言提供了垃圾回收机制,可以自行选择时机释放内存,例如: Go、Java已经实现垃圾回收, C语言目前尚未实现垃圾回收,C++中可以通过智能指针达到垃圾回收的目的。除了语言层面的内存管理外,有时我们需要在程序中自行管理内存,总体而言,对于内存管理,我认为主要是解决以下问题:用户申请内存时,如何快速查找到满足用户需求的内存块?用户释放内存时,如何避免内存碎片化?无论是语言层面实现的内存管理还是应用程序自行实现的内存管理,大都将内存按照大小分为几种,每种采用不同的管理模式。常见的分类是按照2的整数次幂分,将不同种类的内存通过链表链接,查询时,从相应大小的链表中寻找,如果找不到,则可以考虑从更大块内存中,拿取一块,将其分为多个小点的内存。当然,对于特别大的内存,语言层面的内存管理可以直接调用内存管理相关的系统调用,应用层面的内存管理则可以直接使用语言层面的内存管理。nginx内存管理整体可以分为2个部分,第一部分是常规的内存池,用于进程平时所需的内存管理;第二部分是共享内存的管理。总体而言,共享内存较内存池要复杂的多。二. nginx内存池管理2.1 说明本部分使用的nginx版本为1.15.3具体源码参见src/core/ngx_palloc.c文件2.2 nginx实现2.2.1 使用流程nginx内存池的使用较为简单,可以分为3步,调用ngx_create_pool函数获取ngx_pool_t指针。//size代表ngx_pool_t一块的大小ngx_pool_t* ngx_create_pool(size_t size, ngx_log_t log)调用ngx_palloc申请内存使用//从pool中申请size大小的内存void ngx_palloc(ngx_pool_t *pool, size_t size)释放内存(可以释放大块内存或者释放整个内存池)//释放从pool中申请的大块内存ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p)//释放整个内存池void ngx_destroy_pool(ngx_pool_t *pool)2.2.2 具体实现如下图所示,nginx将内存分为2种,一种是小内存,一种是大内存,当申请的空间大于pool->max时,我们认为是大内存空间,否则是小内存空间。//创建内存池的参数size减去头部管理结构ngx_pool_t的大小pool->max = size - sizeof(ngx_pool_t);对于小块内存空间, nginx首先查看当前内存块待分配的空间中,是否能够满足用户需求,如果可以,则直接将这部分内存返回。如果不能满足用户需求,则需要重新申请一个内存块,申请的内存块与当前块空间大小相同,将新申请的内存块通过链表链接到上一个内存块,从新的内存块中分配用户所需的内存。小块内存并不释放,用户申请后直接使用,即使后期不再使用也不需要释放该内存。由于用户有时并不知道自己使用的内存块是大是小,此时也可以调用ngx_pfree函数释放该空间,该函数会从大空间链表中查找内存,找到则释放内存。对于小内存而言,并未做任何处理。对于大块内存, nginx会将这些内存放到链表中存储,通过pool->large进行管理。值得注意的是,用户管理大内存的ngx_pool_large_t结构是从本内存池的小块内存中申请而来,也就意味着无法释放这些内存,nginx则是直接复用ngx_pool_large_t结构体。当用户需要申请大内存空间时,利用c函数库malloc申请空间,然后将其挂载某个ngx_pool_large_t结构体上。nginx在需要一个新的ngx_pool_large_t结构时,会首先pool->large链表的前3个元素中,查看是否有可用的,如果有则直接使用,否则新建ngx_pool_large_t结构。三. nginx共享内存管理3.1 说明本部分使用的nginx版本是1.15.3本部分源码详见src/core/ngx_slab.c, src/core/ngx_shmtx.cnginx共享内存内容相对较多,本文仅做简单概述。3.2 直接使用共享内存3.2.1 基础nginx中需要创建互斥锁,用于后面多进程同步使用。除此之外,nginx可能需要一些统计信息,例如设置(stat_stub),对于这些变量,我们并不需要特意管理,只需要开辟共享空间后,直接使用即可。设置stat_stub后所需的统计信息,亦是放到共享内存中,我们此处仅以nginx中的互斥锁进行说明。3.2.2 nginx互斥锁的实现nginx互斥锁,有两种方案,当系统支持原子操作时,采用原子操作,不支持时采用文件锁。本节源码见ngx_event_module_init函数。下图为文件锁实现互斥锁的示意图。下图为原子操作实现互斥锁的示意图。问题reload时,新启动的master向老的master发送信号后直接退出,旧的master,重新加载配置(ngx_init_cycle函数), 新创建工作进程, 新的工作进程与旧的工作进程使用的锁是相同的。平滑升级时, 旧的master会创建新的master, 新的master会继承旧的master监听的端口(通过环境变量传递监听套接字对应的fd),新的进程并没有重新绑定监听端口。可能存在新老worker同时监听某个端口的情况,此时操作系统会保证只会有一个进程处理该事件(虽然epoll_wait都会被唤醒)。3.3 通过slab管理共享内存nginx允许各个模块开辟共享空间以供使用,例如ngx_http_limit_conn_module模块。nginx共享内存管理的基本思想有:将内存按照页进行分配,每页的大小相同, 此处设为page_size。将内存块按照2的整数次幂进行划分, 最小为8bit, 最大为page_size/2。例如,假设每页大小为4Kb, 则将内存分为8, 16, 32, 64, 128, 256, 512, 1024, 2048共9种,每种对应一个slot, 此时slots数组的大小n即为9。申请小块内存(申请内存大小size <= page_size/2)时,直接给用户这9种中的一种,例如,需要30bit时,找大小为32的内存块提供给用户。每个页只会划分一种类型的内存块。例如,某次申请内存时,现有内存无法满足要求,此时会使用一个新的页,则这个新页此后只会分配这种大小的内存。通过双向链表将所有空闲的页连接。图中ngx_slab_pool_t中的free变量即使用来链接空闲页的。通过slots数组将所有小块内存所使用的页链接起来。对于大于等于页面大小的空间请求,计算所需页数,找到连续的空闲页,将空闲页的首页地址返回给客户使用,通过每页的管理结构ngx_slab_page_t进行标识。所有页面只会有3中状态,空闲、未满、已满。空闲,未满都是通过双向链表进行整合,已满页面则不存在与任何页面,当空间被释放时,会将其加入到某个链表。nginx共享内存的基本结构图如下:在上图中,除了最右侧的ngx_slab_pool_t接口开始的一段内存位于共享内存区外,其他内存都不是共享内存。共享内存最终是从page中分配而来。

November 15, 2018 · 1 min · jiezi

【Nginx源码分析】Nginx中的锁与原子操作

李乐问题引入多线程或者多进程程序访问同一个变量时,需要加锁才能实现变量的互斥访问,否则结果可能是无法预期的,即存在并发问题。解决并发问题通常有两种方案:1)加锁:访问变量之前加锁,只有加锁成功才能访问变量,访问变量之后需要释放锁;这种通常称为悲观锁,即认为每次变量访问都会导致并发问题,因此每次访问变量之前都加锁。2)原子操作:只要访问变量的操作是原子的,就不会导致并发问题。那表达式么i++是不是原子操作呢?nginx通常会有多个worker处理请求,多个worker之间需要通过抢锁的方式来实现监听事件的互斥处理,由函数ngx_shmtx_trylock实现抢锁逻辑,代码如下:ngx_uint_t ngx_shmtx_trylock(ngx_shmtx_t *mtx){ return (mtx->lock == 0 && ngx_atomic_cmp_set(mtx->lock, 0, ngx_pid));}变量mtx->lock指向的是一块共享内存地址(所有worker都可以访问);worker进程会尝试设置变量mtx->lock的值为当前进程号,如果设置成功,则说明抢锁成功,否则认为抢锁失败。注意ngx_atomic_cmp_set设置变量mtx->lock的值为当前进程号并不是无任何条件的,而是只有当变量mtx->lock值为0时才设置,否则不予设置。ngx_atomic_cmp_set是典型的比较-交换操作,且必须加锁或者是原子操作才行,函数实现方式下节分析。nginx有一些全局统计变量,比如说变量ngx_connection_counter,此类变量由所有worker进程共享,并发执行累加操作,由函数ngx_atomic_fetch_add实现;而该累加操作需要加锁或者时原子操作才行,函数实现方式下节分析。上面说的mtx->lock和ngx_connection_counter都是共享变量,所有worker进程都可以访问,这些变量在ngx_event_core_module模块的ngx_event_module_init函数创建,且该函数在fork worker进程之前执行。/ cl should be equal to or greater than cache line size /cl = 128;size = cl / ngx_accept_mutex */ + cl /*ngx_connection_counter / + cl; / ngx_temp_number */ if (ngx_shm_alloc(&shm) != NGX_OK) { return NGX_ERROR;}shared = shm.addr;if (ngx_shmtx_create(&ngx_accept_mutex, (ngx_shmtx_sh_t *) shared,cycle->lock_file.data)!= NGX_OK) { return NGX_ERROR; } ngx_connection_counter = (ngx_atomic_t *) (shared + 1 * cl);这里需要重点思考这么几个问题:1)cache_line_size是什么?我们都知道CPU与主存之间还存在着高速缓存,高速缓存的访问速率高于主存访问速率,因此主存中部分数据会被缓存在高速缓存中,CPU访问数据时会先从高速缓存中查找,如果没有命中才会访问主从。需要注意的是,主存中的数据并不是一字节一字节加载到高速缓存中的,而是每次加载一个数据块,该数据块的大小就称为cache_line_size,高速缓存中的这块存储空间称为一个缓存行。cache_line_size32字节,64字节不等,通常为64字节。2)此处cl取值128字节,可是cl为什么一定要大于等于cache_line_size?待下一节分析了原子操作函数实现方式后自然会明白的。3)函数ngx_shm_alloc是通过系统调用mmap分配的内存空间,首地址为shared;4)这里创建了三个共享变量ngx_accept_mutex、ngx_connection_counter和ngx_temp_number;函数ngx_shmtx_create使得ngx_accept_mutex->lock变量指向shared;ngx_connection_counter指向shared+128字节位置处,ngx_temp_number指向shared+256字节位置处。原子操作函数实现方式据说gcc某版本以后内置了一些原子性操作函数(没有验证),如://原子加type sync_fetch_and_add (type ptr, type value);//原子减type __sync_fetch_and_sub (type ptr, type value);//原子比较-交换,返回truebool __sync_bool_compare_and_swap(type ptr, type oldValue, type newValue, ….);//原子比较交换,返回之前的值type __sync_val_compare_and_swap(type ptr, type oldValue, type newValue, ….);通过这些函数很容易解决上面说的多个worker抢锁,统计变量并发累计问题。nginx会检测系统是否支持上述方法,如果不支持会自己实现类似的原子性操作函数。源码目录下src/os/unix/ngx_gcc_atomic_amd64.h、src/os/unix/ngx_gcc_atomic_x86.h等文件针对不同操作系统实现了若干原子性操作函数。内联汇编可通过内联汇编向C代码中嵌入汇编语言。原子操作函数内部都使用到了内联汇编,因此这里需要做简要介绍;内联汇编格式如下,需要了解以下6个概念:asm ( 汇编指令: 输出操作数(可选): 输入操作数(可选): 寄存器列表(表明哪些寄存器被修改,可选));1)寄存器通常有一些简称;r:表示使用一个通用寄存器,由GCC在%eax/%ax/%al, %ebx/%bx/%bl, %ecx/%cx/%cl, %edx/%dx/%dl中选取一个GCC认为合适的。a:表示使用%eax / %ax / %alb:表示使用%ebx / %bx / %blc:表示使用%ecx / %cx / %cld:表示使用%edx / %dx / %dlm: 表示内存地址等2)汇编指令;" popl %0 "" movl %1, %%esi "" movl %2, %%edi “3)输入操作数,通常格式为——“寄存器简称/内存简称”(值);这种称为寄存器约束或者内存约束,表明输入或者输出需要借助寄存器或者内存实现。: “m” (*lock), “a” (old), “r” (set)4)输出操作数;//+号表示既是输入参数又是输出参数:"+r” (add)//将寄存器%eax / %ax / %al存储到变量res中:"=a" (res)5)寄存器列表,如: “cc”, “memory"cc表示会修改标志寄存器中的条件标志,memory表示会修改内存。6)占位符与volatile关键字__asm volatile ( " xaddl %0, %1; " : “+r” (add) : “m” (*value) : “cc”, “memory”);volatile表明禁止编译器优化;%0和%1顺序对应后面的输出或输入操作数,如%0对应”+r" (add),%1对应"m" (*value)。比较-交换原子实现现代处理器都提供了比较-交换汇编指令cmpxchgl r, [m],且是原子操作。其含义如下为,如果eax寄存器的内容与[m]内存地址内容相等,则设置[m]内存地址内容为r寄存器的值。伪代码如下(标志寄存器zf位):if (eax == [m]) { zf = 1; [m] = r;} else { zf = 0; eax = [m];}因此利用指令cmpxchgl可以很容易实现原子性的比较-交换功能。但是想想这样有什么问题呢?对于单核CPU来说没任何问题,多核CPU则无法保证。(参考深入理解计算机系统第六章)以Intel Core i7处理器为例,其有四个核,且每个核都有自己的L1和L2高速缓存。前面提到,主存中部分数据会被缓存在高速缓存中,CPU访问数据时会先从高速缓存中查找;那假如同一块内存地址同时被缓存在核0与核1的L2级高速缓存呢?此时如果核0与核1同时修改该地址内容,则会造成冲突。目前处理器都提供有lock指令;其可以锁住总线,其他CPU对内存的读写请求都会被阻塞,直到锁释放;不过目前处理器都采用锁缓存替代锁总线(锁总线的开销比较大),即lock指令会锁定一个缓存行。当某个CPU发出lock信号锁定某个缓存行时,其他CPU会使它们的高速缓存该缓存行失效,同时检测是对该缓存行中数据进行了修改,如果是则会写所有已修改的数据;当某个高速缓存行被锁定时,其他CPU都无法读写该缓存行;lock后的写操作会及时会写到内存中。以文件src/os/unix/ngx_gcc_atomic_x86.h为例。查看ngx_atomic_cmp_set函数实现如下:#define NGX_SMP_LOCK “lock;“static ngx_inline ngx_atomic_uint_tngx_atomic_cmp_set(ngx_atomic_t *lock, ngx_atomic_uint_t old, ngx_atomic_uint_t set){ u_char res; asm volatile ( NGX_SMP_LOCK " cmpxchgl %3, %1; " " sete %0; " : “=a” (res) : “m” (*lock), “a” (old), “r” (set) : “cc”, “memory”); return res;}cmpxchgl即为上面说的原子比较-交换指令;sete取标志寄存器中ZF位的值,并存储在%0对应的操作数。函数最后返回标志寄存器zf位。累加指令格式为xaddl r [m],含义如下:temp = [m];[m] += r;r = temp;查看ngx_atomic_fetch_add函数实现:static ngx_inline ngx_atomic_int_tngx_atomic_fetch_add(ngx_atomic_t *value, ngx_atomic_int_t add){ asm volatile ( NGX_SMP_LOCK " xaddl %0, %1; " : “+r” (add) : “m” (*value) : “cc”, “memory”); return add;}指令xaddl实现了加法功能,其将%0对应操作数加到%1对应操作数,函数最后返回累加之前的旧值。这里再回到第一小节,cl取值128字节,且注释表明cl一定要大于等于cache_line_size。cl是什么?三个共享变量之间的偏移量。那假如去掉这个限制,由于每个变量只占8字节,所以三个变量总共占24字节,假设cache_line_size即缓存行大小为64字节,即这三个共享变量可能属于同一个缓存行。那么当使用lock指令锁定ngx_accept_mutex->lock变量时,会锁定该变量所在的缓存行,从而导致对共享变量ngx_connection_counter和ngx_temp_number同样执行了锁定,此时其他CPU是无法访问这两个共享变量的。因此这里会限制cl大于等于缓存行大小。总结本文简要介绍了nginx中锁的实现原理,多核高速缓存冲突问题,内联汇编简单语法,以及原子比较-交换操作和原子累加操作的实现。才疏学浅,如有错误或者不足,请指出。 ...

November 14, 2018 · 2 min · jiezi

Nginx核心知识100讲知识图谱

November 14, 2018 · 0 min · jiezi

【Nginx源码分析】Nginx的listen处理流程分析

施洪宝一. 基础nginx源码采用1.15.5后续部分仅讨论http中的listen配置解析以及优化流程1.1 概述假设nginx http模块的配置如下http{ server { listen 127.0.0.1:8000; server_name www.baidu.com; root html; location /{ index index.html; } } server { listen 10.0.1.1:8000; server_name www.news.baidu.com; root html; location /{ index index.html; } } server { listen 8000; #相当于0.0.0.0:8000 server_name www.tieba.baidu.com; root html; location /{ index index.html; } } server { listen 127.0.0.1:8000; server_name www.zhidao.baidu.com; location / { root html; index index.html; } }}端口, 地址, server的关系端口是指一个端口号, 例如上面的8000端口地址是ip+port, 例如127.0.0.1:8000, 10.0.1.1:8000, 0.0.0.0:8000, listen后配置的是一个地址。每个地址可以放到多个server中, 例如上面的127.0.0.1:8000总而言之, 一个端口可以有多个地址, 每个地址可以有多个server1.2 存在的问题是否需要在读取完http块中所有的server才能建立监听套接字, 绑定监听地址?是的, 因为允许配置通配地址, 故而必须将http块中的server全部读取完后, 才能知道如何建立监听套接字。一个端口可以对应多个地址, 如何建立监听套接字, 如何绑定地址?通常情况下, 每个地址只能绑定一次(只考虑tcp协议), 这种情况下, 我们只能选择部分地址创建监听套接字, 绑定监听地址。当配置中存在通配地址(0.0.0.0:port)时, 只需要创建一个监听套接字, 绑定这个通配地址即可, 但需要能够依据该监听套接字找到该端口配置的其他地址, 这样当客户端发送请求时, 可以根据客户端请求的地址, 找到对应地址下的相关配置。当配置中不存在通配地址时, 需要对每个地址都创建一个监听套接字, 绑定监听地址。一个地址多个server的情况下, 如何快速找到客户端请求的server?比较合适的方案是通过hash表。为了快速找到客户端请求的server, nginx以server_name为key, 每个server块的配置(可以理解为一个指针, 该指针指向整个server块的配置)为value, 放入到哈希表。由于server_name中可以出现正则匹配等情况, nginx将server_name具体分为4类进行分别处理(www.baidu.com, baidu.com, www.baidu, ~*baidu)。1.3 nginx listen解析的流程总体而言分为2步,将所有http模块内的配置解析完成, 将listen的相关配置暂存(主要存储监听端口以及监听地址)。根据上一步暂存的监听端口以及监听地址, 创建监听套接字, 绑定监听地址二. 配置解析nginx http块解析完成后, 会存储配置文件中配置的监听端口以及监听地址, 其核心结构图如下,总体而言, 结构可以分为3级, 端口->地址->server2.1 源码listen的处理流程:ngx_http_core_listen: 读取配置文件配置ngx_http_add_listen: 查看之前是否出现过当前监听的端口, 没有则新建, 否则追加ngx_http_add_address: 查看之前该端口下是否监听过该地址, 没有则新建, 否则追加。ngx_http_add_server: 查看server之前是否出现过, 没有则新建, 否则报错(重复定义)。三. 创建监听套接字nginx最终创建的监听套接字及其相关的结构图如下,每个ngx_listening_t结构对应一个监听套接字, 绑定一个监听地址每个ngx_listening_t结构后面需要存储地址信息, 地址可能不止一个, 因为这个监听套接字可能绑定的是通配地址, 这个端口下的其他地址都会放在这个监听套接字下。例如, 1.1节的配置中, 只会创建一个ngx_listening_t结构, 其他地址的配置都会放到这个通配地址下。每个监听地址可能对应多个域名(配置文件中的server_name), 需要将这些域名放到哈希表中, 以供后续使用总体而言, 结构分为3级, 监听套接字->监听地址->server3.1 源码读取完http块后, 需要创建监听套接字绑定监听地址, 处理函数ngx_http_optimize_servers, 该函数的处理流程:遍历所有监听端口, 针对每个监听端口, 执行以下3步对该端口下所有监听地址排序(listen后配置bind的放在前面, 通配地址放在后面)遍历该端口下的所有地址, 将每个地址配置的所有server, 放到该地址的哈希表中。为该端口建立监听套接字, 绑定监听地址。四. 监听套接字的使用假设此处我们使用epoll作为事件处理模块epoll在增加事件时, 用户可以使用epoll_event中的data字段, 当事件发生时, 该字段也会带回。nginx中的epoll_event指向的是ngx_connection_t结构, 事件发生时, 调用ngx_connection_t结构中的读写事件, 负责具体处理事件, 参见下图。//c is ngx_connection_trev = c->read;rev->hadler(rev);wev = c->write;wev->handler(wev);每个监听套接字对应一个ngx_connection_t, 该结构的读事件回调函数为ngx_event_accept, 当用户发起tcp握手时, 通过ngx_event_accept接受客户端的连接请求。ngx_event_accept会接受客户端请求, 初始化一个新的ngx_connection_t结构, 并将其加入到epoll中进行监听, 最后会调用ngx_connection_t对应的ngx_listening_t的处理函数(http块对应ngx_http_init_connection, mail块ngx_mail_init_connection, stream块对应ngx_stream_init_connection)五. 总结nginx在读取listen相关的配置时, 将结构分为3级, 端口->地址->server, 各级都是一对多的关系。nginx在创建监听套接字时, 将结构分为3级, 监听套接字->地址->server, 各级都是一对多的关系。 ...

November 13, 2018 · 1 min · jiezi

nginx常用功能全揭秘(内附福利!!!)

本文旨在用最通俗的语言讲述最枯燥的基本知识nginx作为一个高性能的web服务器,想必大家垂涎已久,蠢蠢欲动,想学习一番了吧,语法不多说,网上一大堆。下面博主就nginx的非常常用的几个功能做一些讲述和分析,学会了这几个功能,平常的开发和部署就不是什么问题了。因此希望大家看完之后,能自己装个nginx来学习配置测试,这样才能真正的掌握它。前方高能,文末有福利。文章提纲:正向代理反向代理透明代理负载均衡静态服务器nginx的安装一点点小福利1. 正向代理正向代理:内网服务器主动去请求外网的服务的一种行为光看概念,可能有读者还是搞不明白:什么叫做“正向”,什么叫做“代理”,我们分别来理解一下这两个名词。正向:相同的或一致的方向代理:自己做不了的事情或者自己不打算做的事情,委托或依靠别人来完成。借助解释,回归到nginx的概念,正向代理其实就是说客户端无法主动或者不打算完成主动去向某服务器发起请求,而是委托了nginx代理服务器去向服务器发起请求,并且获得处理结果,返回给客户端。从下图可以看出:客户端向目标服务器发起的请求,是由代理服务器代替它向目标主机发起,得到结果之后,通过代理服务器返回给客户端。举个栗子:广大社会主义接班人都知道,为了保护祖国的花朵不受外界的乌烟瘴气熏陶,国家对网络做了一些“优化”,正常情况下是不能外网的,但作为程序员的我们如果没有谷歌等搜索引擎的帮助,再销魂的代码也会因此失色,因此,网络上也曾出现过一些fan qiang技术和软件供有需要的人使用,如某VPN等,其实VPN的原理大体上也类似于一个正向代理,也就是需要访问外网的电脑,发起一个访问外网的请求,通过本机上的VPN去寻找一个可以访问国外网站的代理服务器,代理服务器向外国网站发起请求,然后把结果返回给本机。正向代理的配置:server { #指定DNS服务器IP地址 resolver 114.114.114.114; #指定代理端口 listen 8080; location / { #设定代理服务器的协议和地址(固定不变) proxy_pass http://$http_host$request_uri; } } 这样就可以做到内网中端口为8080的服务器主动请求到1.2.13.4的主机上,如在Linux下可以:curl –proxy proxy_server:8080 http://www.taobao.com/正向代理的关键配置:resolver:DNS服务器IP地址listen:主动发起请求的内网服务器端口proxy_pass:代理服务器的协议和地址2. 反向代理反向代理:reverse proxy,是指用代理服务器来接受客户端发来的请求,然后将请求转发给内网中的上游服务器,上游服务器处理完之后,把结果通过nginx返回给客户端。上面讲述了正向代理的原理,相信对于反向代理,就很好理解了吧。反向代理是对于来自外界的请求,先通过nginx统一接受,然后按需转发给内网中的服务器,并且把处理请求返回给外界客户端,此时代理服务器对外表现的就是一个web服务器,客户端根本不知道“上游服务器”的存在。举个栗子:一个服务器的80端口只有一个,而服务器中可能有多个项目,如果A项目是端口是8081,B项目是8082,C项目是8083,假设指向该服务器的域名为www.xxx.com,此时访问B项目是www.xxx.com:8082,以此类推其它项目的URL也是要加上一个端口号,这样就很不美观了,这时我们把80端口给nginx服务器,给每个项目分配一个独立的子域名,如A项目是a.xxx.com,并且在nginx中设置每个项目的转发配置,然后对所有项目的访问都由nginx服务器接受,然后根据配置转发给不同的服务器处理。具体流程如下图所示:反向代理配置:server { #监听端口 listen 80; #服务器名称,也就是客户端访问的域名地址 server_name a.xxx.com; #nginx日志输出文件 access_log logs/nginx.access.log main; #nginx错误日志输出文件 error_log logs/nginx.error.log; root html; index index.html index.htm index.php; location / { #被代理服务器的地址 proxy_pass http://localhost:8081; #对发送给客户端的URL进行修改的操作 proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504; proxy_max_temp_file_size 0; }}这样就可以通过a.xxx.com来访问a项目对应的网站了,而不需要带上难看的端口号。反向代理的配置关键点是:server_name:代表客户端向服务器发起请求时输入的域名proxy_pass:代表源服务器的访问地址,也就是真正处理请求的服务器(localhost+端口号)。3. 透明代理透明代理:也叫做简单代理,意思客户端向服务端发起请求时,请求会先到达透明代理服务器,代理服务器再把请求转交给真实的源服务器处理,也就是是客户端根本不知道有代理服务器的存在。举个栗子:它的用法有点类似于拦截器,如某些制度严格的公司里的办公电脑,无论我们用电脑做了什么事情,安全部门都能拦截我们对外发送的任何东西,这是因为电脑在对外发送时,实际上先经过网络上的一个透明的服务器,经过它的处理之后,才接着往外网走,而我们在网上冲浪时,根本没有感知到有拦截器拦截我们的数据和信息。有人说透明代理和反向代理有点像,都是由代理服务器先接受请求,再转发到源服务器。其实本质上是有区别的,透明代理是客户端感知不到代理服务器的存在,而反向代理是客户端感知只有一个代理服务器的存在,因此他们一个是隐藏了自己,一个是隐藏了源服务器。事实上,透明代理和正向代理才是相像的,都是由客户端主动发起请求,代理服务器处理;他们差异点在于:正向代理是代理服务器代替客户端请求,而透明代理是客户端在发起请求时,会先经过透明代理服务器,再达到服务端,在这过程中,客户端是感知不到这个代理服务器的。4. 负载均衡负载均衡:将服务器接收到的请求按照规则分发的过程,称为负载均衡。负载均衡是反向代理的一种体现。可能绝大部分人接触到的web项目,刚开始时都是一台服务器就搞定了,但当网站访问量越来越大时,单台服务器就扛不住了,这时候需要增加服务器做成集群来分担流量压力,而在架设这些服务器时,nginx就充当了接受流量和分流的作用了,当请求到nginx服务器时,nginx就可以根据设置好的负载信息,把请求分配到不同的服务器,服务器处理完毕后,nginx获取处理结果返回给客户端,这样,用nginx的反向代理,即可实现了负载均衡。nginx实现负载均衡有几种模式:轮询:每个请求按时间顺序逐一分配到不同的后端服务器,也是nginx的默认模式。轮询模式的配置很简单,只需要把服务器列表加入到upstream模块中即可。下面的配置是指:负载中有三台服务器,当请求到达时,nginx按照时间顺序把请求分配给三台服务器处理。upstream serverList { server 1.2.3.4; server 1.2.3.5; server 1.2.3.6;}ip_hash:每个请求按访问IP的hash结果分配,同一个IP客户端固定访问一个后端服务器。可以保证来自同一ip的请求被打到固定的机器上,可以解决session问题。下面的配置是指:负载中有三台服务器,当请求到达时,nginx优先按照ip_hash的结果进行分配,也就是同一个IP的请求固定在某一台服务器上,其它则按时间顺序把请求分配给三台服务器处理。upstream serverList { ip_hash server 1.2.3.4; server 1.2.3.5; server 1.2.3.6;}url_hash:按访问url的hash结果来分配请求,相同的url固定转发到同一个后端服务器处理。upstream serverList { server 1.2.3.4; server 1.2.3.5; server 1.2.3.6; hash $request_uri; hash_method crc32; }fair:按后端服务器的响应时间来分配请求,响应时间短的优先分配。upstream serverList { server 1.2.3.4; server 1.2.3.5; server 1.2.3.6; fair;}而在每一种模式中,每一台服务器后面的可以携带的参数有:down: 当前服务器暂不参与负载weight: 权重,值越大,服务器的负载量越大。max_fails:允许请求失败的次数,默认为1。fail_timeout:max_fails次失败后暂停的时间。backup:备份机, 只有其它所有的非backup机器down或者忙时才会请求backup机器。如下面的配置是指:负载中有三台服务器,当请求到达时,nginx按时间顺序和权重把请求分配给三台服务器处理,例如有100个请求,有30%是服务器4处理,有50%的请求是服务器5处理,有20%的请求是服务器6处理。upstream serverList { server 1.2.3.4 weight=30; server 1.2.3.5 weight=50; server 1.2.3.6 weight=20;}如下面的配置是指:负载中有三台服务器,服务器4的失败超时时间为60s,服务器5暂不参与负载,服务器6只用作备份机。upstream serverList { server 1.2.3.4 fail_timeout=60s; server 1.2.3.5 down; server 1.2.3.6 backup;}下面是一个配置负载均衡的示例(只写了关键配置):其中:upstream serverList是负载的配置模块server_name是客户端请求的域名地址proxy_pass是指向负载的列表的模块upstream serverList { server 1.2.3.4 weight=30; server 1.2.3.5 down; server 1.2.3.6 backup;} server { listen 80; server_name www.xxx.com; root html; index index.html index.htm index.php; location / { proxy_pass http://serverList; proxy_redirect off; proxy_set_header Host $host; }}5. 静态服务器现在很多项目流行前后分离,也就是前端服务器和后端服务器分离,分别部署,这样的方式能让前后端人员能各司其职,不需要互相依赖,而前后分离中,前端项目的运行是不需要用Tomcat、Apache等服务器环境的,因此可以直接用nginx来作为静态服务器。静态服务器的配置如下,其中关键配置为:root:直接静态项目的绝对路径的根目录。server_name : 静态网站访问的域名地址。server { listen 80; server_name www.xxx.com; client_max_body_size 1024M; location / { root /var/www/xxx_static; index index.html; } }nginx的安装学了这么多nginx的配置用法之后,我们需要对每一个知识点做一下测试,才能印象深刻,在此之前,我们需要知道nginx是怎么安装,下面以Linux环境为例,简述yum方式安装nginx的步骤:安装依赖://一键安装上面四个依赖yum -y install gcc zlib zlib-devel pcre-devel openssl openssl-devel安装nginx:yum install nginx检查是否安装成功:nginx -v启动/挺尸nginx:/etc/init.d/nginx start/etc/init.d/nginx stop编辑配置文件:/etc/nginx/nginx.conf 这些步骤都完成之后,我们就可以进入nginx的配置文件nginx.conf对上面的各个知识点,进行配置和测试了。一点点小福利:有的人说看到这里好像都懂了,但是没Linux可以测试呀,其实nginx也有Windows版本,当然Windows版本在实际项目中用的并不多,因为web项目的服务器都是Linux,因此你可以在自己的电脑上装个虚拟机,然后装个Linux来玩耍,不过如果如果你想要学以致用,并且更接近公司项目的生产环境的配备的话,作者建议你还是自己买一台云服务器来学习,刚好阿里云有双十一活动,一台服务器只需要90元一年,这是明显的撸羊毛活动啊,你忍心放弃吗?你花在装虚拟机和Linux系统上的时间都不止90块钱了!你有没有想过有朝一日亲手上传自己的代码到服务器呢?你有没有憧憬拥有自己搭建和维护的个人博客呢?你试过从项目发布上线到域名解析访问整个流程吗?….如果都没有,建议你买一台吧:点我90元购买服务器如果链接无法点开,请扫描二维码购买:————划重点—————-注意了!!!点链接注册购买之后,阿里云会有返现给博主,为了感谢各位大大的厚爱,博主决定把所有返现归还给各位大大,因此凡是新用户注册购买的,请加博主V(sisi-ceo)联系博主,博主核实后把返现以红包的形式给你您,让你最低的价格买到服务器,能学到更多的东西!!!(老用户以及点别的链接注册购买的大大,博主是收不到返现的哦,所以这部分博主无能为力的啦~~~)觉得本文对你有帮助?请分享给更多人关注「编程无界」,提升装逼技能 ...

November 12, 2018 · 1 min · jiezi

Debian9(Stretch) 下编译安装LNMP环境

Debian9下源码安装LNMP一、前言之前,我的开发环境是Windows-10+PHP-7.1+Nginx-1.10+MariaDB-10.1。后面开发需要使用到memcached,redis等nosql比较多,而在Windows下定制难度,很多PHP拓展并没有.dll文件,且PHP拓展在Windows下compile难度还是比较大的。所以促使我转向Linux下开发。首先,我search了一下,主要是Red Hat 与Debian。基于Red Hat:商业版,Centos,Fedora基于Debian: Debian,Ubuntu我选择了Debian 9,PHP-7.2,MariaDB-10.2,Nginx-1.13二、Requirements一般安装顺序,mariadb > nginx > php,以下的涉及的软件,库名均是基于Debian(Ubuntu)。2.1 PHP的需要的额外库:## 源码需要的词法分析器apt install bison## 源码都是c程序,需要c编译器,注意编译器版本apt install gcc-6## C++编译器apt install g++## xml解析库apt install libxml2 libxml2-dev## make cmake m4 autoconfapt install make cmake m4 autoconf## webp 格式,能够带来更小体积的图片apt install libwebp6 libwebp-dev## jpeg格式支持apt install libjpeg-dev## png格式支持apt install libpng-dev## 免费开源字体引擎apt install libfreetype6 libfreetype6-dev## ssl加密库支持(源码安装openssl,可以选择使用Debian 包来安装openssl)apt install openssl## ssh2 库(源码安装)apt install libssh2-1-dev## mhash 库apt install libmhash2## zlib 压缩库(源码安装)apt install zlib1g zlib1g-dev## pcre 正则表达式库(源码安装)apt install libpcre3-dev libpcre3## gzipapt install gzip## bz2apt install libbz2-1.0 libbz2-dev## soduim php7.2新特性 现代加密标准apt install libsodium-dev## argon2 php7.2新特性 新的加密函数,由PHC(Password Hashing Competition)发布apt install argon2 libargon2-0 libargon2-0-dev2.2 Nginx 需要的额外库主要是三个,openssl,zlib,pcre,可以通过Debian本身的库安装,也可以选择源码安装。我选择后者,所以,并不会与上面的冲突,后面会涉及到原因。2.3 MariaDB 需要的额外库## bison词法分析器apt install bison## libncurses 一个可用于编写独立终端基于文本的的程序库apt install libncurses5 libncurses5-dev## libevent-dev 一个事件库apt install libevent-dev## openssl 一个加密库apt install openssl三、 安装过程按照MariaDB > Nginx > PHP的顺序安装,安装前请再次检查上述所需的额外库都已安装。3.1 对应的系统用户创建为什么要创建用户? 答:因为安装完成后,我们只需要这些程序只用于系统服务就好(daemon或者其他自己运行的进程),并不需要使用一个具体用户身份去操作他。即创建系统账户,以及系统用户组。groupadd -r mysqluseradd -r -g mysql -s /bin/false -M mysqlmkdir /usr/local/data/mysqlchown -R mysql:mysql /usr/local/data/mysql note 参数含义通过man groupadd 或者man useradd 可以调出具体的手册-r 创建系统用户或者用户组-g 指定用户所属用户组-s 指定用户登录shell名字,sh,bash,因为是系统用户,并不需要,设置 /bin/false或者/usr/sbin/nologin-M 不创建用户主目录同样,分别创建nginx,php-fpmgroupadd -r php-fpmuseradd -r -g php-fpm -s /bin/false -M php-fpmgroupadd -r nginxuseradd -r -g nginx -s /bin/false -M nginx 3.2 MariaDBMariaDB 安装可能略显麻烦,并不是常见的make方式,而是cmake方式。获取mariadb-10.2源码wget http://mirror.jaleco.com/mariadb//mariadb-10.2.12/source/mariadb-10.2.12.tar.gz tar -zxvf mariadb-10.2.12.tar.gzmkdir build-mariadbcd build-mariadbcmake ../ -DCMAKE_INSTALL_PREFIX=/opt/soft/mariadb-10.3.4 -DMYSQL_DATADIR=/var/data/mysql -DSYSCONFDIR=/etc -DWITHOUT_TOKUDB=1 -DWITH_INNOBASE_STORAGE_ENGINE=1 -DWITH_ARCHIVE_STPRAGE_ENGINE=1 -DWITH_BLACKHOLE_STORAGE_ENGINE=1 -DWIYH_READLINE=1 -DWIYH_SSL=system -DVITH_ZLIB=system -DWITH_LOBWRAP=0 -DMYSQL_UNIX_ADDR=/tmp/mysql.sock -DDEFAULT_CHARSET=utf8 -DDEFAULT_COLLATION=utf8_general_ci -DBUILD_LIBPROTOBUF_FROM_SOURCES=ONmake && make install 如果失败 使用 rm -rf CMakeCache.txt3.2.1 配置MariaDBvim /etc/profile.d/mariadb.shaddexport PATH=$PATH:/opt/soft/mariadb-10.2/binsource /etc/profile.d/mariadb.shcd /opt/soft/mariadb-10.2scripts/mysql_install_db –user=mysql –datadir=/usr/local/data/mysql成功输出信息:Installing MariaDB/MySQL system tables in ‘/data/mysql’ …OKTo start mysqld at boot time you have to copysupport-files/mysql.server to the right place for your systemPLEASE REMEMBER TO SET A PASSWORD FOR THE MariaDB root USER !To do so, start the server, then issue the following commands:’./bin/mysqladmin’ -u root password ’new-password’’./bin/mysqladmin’ -u root -h localhost.localdomain password ’new-password’Alternatively you can run:’./bin/mysql_secure_installation’which will also give you the option of removing the testdatabases and anonymous user created by default. This isstrongly recommended for production servers.See the MariaDB Knowledgebase at http://mariadb.com/kb or theMySQL manual for more instructions.You can start the MariaDB daemon with:cd ‘.’ ; ./bin/mysqld_safe –datadir=’/data/maria’You can test the MariaDB daemon with mysql-test-run.plcd ‘./mysql-test’ ; perl mysql-test-run.plPlease report any problems at http://mariadb.org/jiraThe latest information about MariaDB is available at http://mariadb.org/.You can find additional information about the MySQL part at:http://dev.mysql.comConsider joining MariaDB’s strong and vibrant community:https://mariadb.org/get-involved/复制cd /opt/soft/mariadb-10.2cp support-files/my-large.cnf /etc/my.cnf或者cp support-files/my-large.cnf /etc/mysql/my.cnf创建系统启动脚本(使用systemd)cd /etc/systemd/systemvim mysqld.service [Unit]Description=MariaDB ServerAfter=network.target[Service]ExecStart=/opt/soft/mariadb-10.2/bin/mysqld –defaults-file=/etc/mysql/my.cnf –datadir=/usr/local/data/mysql –socket=/tmp/mysql.sockUser=mysqlGroup=mysqlWorkingDirectory=/opt/soft/mariadb-10.2[Install]WantedBy=multi-user.targetsystemctl daemon-reloadsystemctl restart mysqld.servicesystemctl status mysqld.servie 如果没有启动,请使用journalctl -xn 或者 journalctl -xl来查看错误信息如果想开机启动,请使用systemctl enable mysqld.service安全设置$:mysql_secure_installation Enter current password for root (enter for none): 输入当前root密码(没有输入)Set root password? [Y/n] 设置root密码?(是/否)New password: 输入新root密码Re-enter new password: 确认输入root密码Password updated successfully! 密码更新成功By default, a MariaDB installation has an anonymous user, allowing anyoneto log into MariaDB without having to have a user account created forthem. This is intended only for testing, and to make the installationgo a bit smoother. You should remove them before moving into aproduction environment.默认情况下,MariaDB安装有一个匿名用户,允许任何人登录MariaDB而他们无需创建用户帐户。这个目的是只用于测试,安装去更平缓一些。你应该进入前删除它们生产环境。Remove anonymous users? [Y/n] 删除匿名用户?(是/否)Normally, root should only be allowed to connect from ’localhost’. Thisensures that someone cannot guess at the root password from the network.通常情况下,root只应允许从localhost连接。这确保其他用户无法从网络猜测root密码。Disallow root login remotely? [Y/n] 不允许root登录远程?(是/否)By default, MariaDB comes with a database named ’test’ that anyone canaccess. This is also intended only for testing, and should be removedbefore moving into a production environment.默认情况下,MariaDB提供了一个名为“测试”的数据库,任何人都可以访问。这也只用于测试,在进入生产环境之前应该被删除。Reloading the privilege tables will ensure that all changes made so farwill take effect immediately.重新加载权限表将确保所有到目前为止所做的更改将立即生效。Reload privilege tables now? [Y/n] 现在重新加载权限表(是/否)All done! If you’ve completed all of the above steps, your MariaDBinstallation should now be secure.全部完成!如果你已经完成了以上步骤,MariaDB安装现在应该安全。Thanks for using MariaDB!至此,mariaddb已经安装完成,可以使用 ps -aux | grep mysql 查看服务现在测试一下,mysql -u root -p 或者 mysql -h localhost -P 5001 -u shanechiu -p 3.3 PHP 安装PHP 安装比较简单,主要是选择你要安装的拓展或者需要开启的功能可以使用./configure –help 来浏览源码安装提供的安装选项有些属于PHP内置的功能,你只需要 enable或者disable,比如php-fpm,是需要启用的。有些拓展是可以动态加载的,称之为shared extension,但是官方也说了,并不是所有的拓展都是能够shared.获取源码:wget http://am1.php.net/distributions/php-7.2.1.tar.bz2解压:tar -xvf php-7.2.1.tar.bz2cd php-7.2.1./configure –prefix=/opt/soft/php7.2 --with-config-file-path=/opt/soft/php7.2/etc --with-mysql-sock=/tmp/mysql.sock --with-openssl --with-mhash --with-mysqli=shared,mysqlnd --with-pdo-mysql=shared,mysqlnd --with-pdo-pgsql=/opt/soft/pgsql --with-gd --with-iconv --with-zlib --enable-exif --enable-intl --enable-calendar --enable-zip --enable-inline-optimization --disable-debug --disable-rpath --enable-shared --enable-xml --enable-bcmath --enable-shmop --enable-mbregex --enable-mbstring --enable-ftp --enable-sysvmsg --enable-sysvsem --enable-sysvshm --enable-pcntl --enable-sockets --enable-ipv6 --with-bz2 --with-xmlrpc --enable-soap --without-pear --with-gettext --enable-session --with-curl=/opt/soft/curl7.57–enable-debug --with-jpeg-dir --with-png-dir --with-freetype-dir --enable-opcache --enable-fpm --with-fpm-user=nginx --with-fpm-group=nginx --with-sodium --with-libxml-dir --with-password-argon2 --without-gdbm --with-pcre-regex --with-pcre-jit --enable-fast-install --enable-fileinfo配置进入源码文件,cp php.ini.development /opt/soft/php-7.2/php.ini修改以下部分extension_dir=/opt/soft/php-7.2/lib/php/extensions/no-debug-non-zts-20170718/extension=mysqlitime_zone=PRC同时要添加php-fpm配置文件,安装目录下 etc/ 下 cp php-fpm.conf.default php-fpm.conf 和 cp php.conf.d/www.conf.default php.conf.d/www.confPHP-FPM启动脚本(systemd)PHP 非常人性化,在源码目录下/sapi/fpm下可以找到php-fpm.service文件,复制到/etc/systemd/system/php-fpm.service中systemdctl start php-fpm.servicesystemdctl status php-fpm.service如果发生错误,使用journalctl -xn查看具体错误信息开机启动,sytemctl enable php-fpm.service3.4 Nginx 源码安装Nginx的编译安装难易程度应该是LNMP环境中最简单的首先需要三个源码包,一个zlib(压缩库),一个pcre(正则表达式库),一个openssl(加密库,如果要使用HTTPS,这个库是必须的),当然你如果是通过debian本身的包管理器安装的,这个可以省略,但是一定要安装两个,一个是软件本身,同时还要安装开发库,像这种,apt -y install openssl opensll-dev。命令:–configure –prefix=/opt/soft/nginx --user=nginx --group=nginx --with-http_ssl_module \ # 这个默认是不开启的,如需使用TLS,请带上这一项编译。–with-pcre=../pcre-8.41 --with-zlib=../zlib-1.2.11 --with-openssl=../openssl-1.1.0g 然后,make 和 make install注意,如果是使用二进制包安装了zlib,pcre,openssl,及相应的开发库,不需要指定路径。配置:编写nginx守护进程文件,还是利用systemd工具vim /etc/sytemd/system/nginx.service[Unit]Description=The NGINX HTTP and reverse proxy serverAfter=syslog.target network.target remote-fs.target nss-lookup.target[Service]Type=forkingPIDFile=/opt/soft/nginx/logs/nginx.pidExecStartPre=/opt/soft/nginx/sbin/nginx -tExecStart=/opt/soft/nginx/sbin/nginxExecReload=/bin/kill -s HUP $MAINPIDExecStop=/bin/kill -s QUIT $MAINPIDPrivateTmp=true[Install]WantedBy=multi-user.target这个可以在nginx 官网找到,可以按照自己需求修改。注意路径修改成自己的安装路径。systemctl start nginx.service 启动Nginxsystemctl enable nginx.service 开机启动记得,如果中途修改了service文件,一定要先运行systemctl daemon-reload重新加载守护进程文件。然后运行 systemctl start nginx.service重启服务。四、APPEND后续会添加一键安装脚本。五、参考资料systemd 入门教程CentOS7.3编译安装MariaDB10.2.6CentOS7.3编译安装php7.1GNU bisonGD-support configure PHPArgon2The Sodium crypto library (libsodium)")get the mariadb code,buildit,test itGeneric Build InstructionsInstalling System Tables (mysql_install_db)")“Compiling MariaDB From Source"ncursesCMakephp-manulPHP7.2 NEW FEATUREBuilding nginx from Sources ...

November 9, 2018 · 4 min · jiezi

基于node开发的web应用,负载均衡的简单实践

集群(cluster)是一组相互独立的、通过高速网络互联的计算机,它们构成了一个组,并以单一系统的模式加以管理。一个客户与集群相互作用时,集群像是一个独立的服务器。负载均衡(Load Balance),其意思就是分摊到多个操作单元上进行执行阿里云负载均衡架构文档负载均衡好处节省成本,一个服务器性能再好也是有瓶颈的,而且性能越高的服务器成本也越大。极大的提高了并发量和响应速度。实践例子学无止境网该web应用,由两个服务器一起提供的服务实现负载均衡遇到的问题nginx负载均衡策略多台服务器代码同步多台服务器数据库同步node服务,代码更新后,服务重启源的代码更新问题和数据升级用户上传的图片等静态资源同步Nginx反向代理及负载均衡轮询权重ip_hashurl_hash等等…这里使用最简单的轮询机制,session存放在数据库,解决了session服务器之间不同步的问题。upstream tianshengjie{ server ip地址; server ip地址 max_fails=2 fail_timeout=10s;}server { listen 80 default_server; server_name 47.99.90.167 www.tianshengjie.cn tianshengjie.cn; location / { proxy_pass http://tianshengjie; proxy_cache_key $http_range$uri$is_args$args; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }}服务部署forever start -c nodemon app.js –exitcrashforeverA simple CLI tool for ensuring that a given script runs continuously守护node进程程序nodemon自动监听文件变化,重启node服务exitcrash,当node服务奔溃后,重启代码同步使用shell脚本,自动更新代码,一键同步更新#!/bin/bashcd git仓库git pull;yarn install –production;rsync -av –exclude-from=/opt/ssh/blog_exclude.list git仓库 代码发布地址rsync -avz -e ssh /var/www/blog/ root@负载均衡服务器ip:负载均衡服务器发布代码目录cd 代码发布地址 ;forever stop app.js;npm run start;echo “发布成功"将git仓库和正式应用的代码地址分离更新git仓库地址下载程序依赖将git仓库更新后的代码复制到正式发布目录将代码同步更新到负载均衡服务器重启服务数据库同步阿里 云数据库文档地址性能最高,有备份有容灾,功能强大,但是收费mysqlmysql远程连接配置配置相对简单,数据库会有性能瓶颈,免费分布式数据库研究中静态资源同步当用户通过负载均衡,被定位到了不同的服务器。这时候,上传文件时,将会把文件上传到不同的服务器中。当用户被分配到了其他服务器时,就会找不到这个文件了。所以我们需要同步负载均衡的服务器的文件。方案一:自己实现统一文件上传管理系统,所有用户文件统一上传到一个地方。方案二:使用阿里云的NAS文件系统管理方案三:使用NFS系统阿里云 NAS文件系统管理阿里云文件存储(Network Attached Storage,简称 NAS)是面向阿里云 ECS 实例、HPC 和 Docker 等计算节点的文件存储服务,提供标准的文件访问协议,您无需对现有应用做任何修改,即可使用具备无限容量及性能扩展、单一命名空间、多共享、高可靠和高可用等特性的分布式文件系统。配置挂载缺点缺点:收费优点配置相对简单弹性伸缩,按量收费阿里出品NFS (Network FileSystem)配置文档缺点配置相对复杂server宕机了所有客户端都不能访问在高并发下NFS效率/性能有限数据是通过明文传送,安全性一般对数据完整性不做验证多台机器挂载NFS服务器时,连接管理维护麻烦优点免费,免费的就是好节省存储空间实现了多台服务器共享文件原文地址:https://tianshengjie.cn/artic… ...

November 5, 2018 · 1 min · jiezi

什么样的经历,才能领悟成为架构师?

最近我发现,无论是博客也好,还是我写的技术专栏也好,经常会收到很多朋友的留言,留言的内容除了讨论技术问题以外,问的最多的,莫过于职业生涯规划相关的了。例如:我刚毕业,如何入行Java开发这一行业?干了几年Java开发了,感觉进入瓶颈期,不知道下一步该怎么走了?大家做生意的做生意,转管理的也不在少数,我还需要坚持做技术么?问题虽然五花八门,但是总结下来就是一个:Java工程师的职业道路该如何走?我尝试着从各个角度回答大家问题,包括夯实基础,并学习其他例如学python、大数据等其他技能。但是这个回答,可能略显乏力,毕竟我提供的更多的是战术方向,即具体的操作方法。可是战略方向,比如把时间线拉长一点,五年,十年该如何规划你的职业生涯呢?我想,这个问题最好的答案,还是需要那些有历经的过来人,才最有资格和大家谈论这个话题。特别凑巧,前几天在微信上与一位前同事叙旧群聊时,他对自己在通往Java架构之路上做了自己的独特分享。他是如何成为一名成功的Java架构师,甚至公司高管和的历程。瞬间我眼前一亮,这不就是包括我在内以及广大同行们所需要的滋补品吗?话不多说,大家请往下看。开场白在同大家分享之前,先让我唠叨两句。虽然工作的事务不同,技术点不同,但是大家都有一个共同的目标,即成为所谓的人生赢家。重要的是,我们需要关注他这快十年的人生路线是如何规划和走下来的,中间有什么可以学习之处?希望你看了他职业生涯经历以后,或许能够对你的当前的职业规划有所帮助,人生有所启迪。故事,就要从头开始,那才精彩。非科班出身的Java架构师——王贤王贤,89年,工作8年,某一线互联网架构师 P7说起王贤,我第一印象就是他的语速很快,说话很有条理,平常人需要掰扯 60 分钟的事情,到他这里,顶多30分钟给你安排的很明白。对于王贤而言,他人生中最不自信的一件事就是学历了。大专出身的他,在找工作这件事上,并没有十分大的优势。而做为大专出身,王贤的每一次机会都来之不易。10 年大专毕业前夕,王贤拉着一个箱子就去了魔都【上海】。「破釜沉舟,没想着怎么回去」,带着自己的简历,王贤跑遍了上海大大小小的互联网公司,最后终于在一家小型互联网公司急招Android工程师的时候趁虚而入,成为了一名初级Android工程师。【大专学的计算机专业:C/C++/Java基础;没什么项目经验】能够拿到这个机会,王贤十分珍惜。所以当遇到「你去开发一个 app,公司暂时不会给你提供额外的资源」的要求时,王贤迎难而上。据王贤当时回忆,【当时在接这个项目的时候,也是硬着头皮,每天都在琢磨着,怎么样才能把这个 app 开发出来。】这对于王贤来说,是他毕业后人生中的第一桶金的项目。为了能做出这个项目,王贤浏览了很多技术网站,学习和初步认识了很多技术相关,最后终于倒腾出一个还不错的版本。【有这样的毅力,值得我们学习】。我依然清楚的记得我第一次见王贤的时候,他就给我留下来深刻的印象。当时在12年的一次技术交流会上认识的,当时我们交谈了很久,也聊了很多。我们各自也聊了自己在技术上的见解与感悟,当时也互留了联系方式。【王贤十分直率地说出了自己独自一人闯荡的心路历程,再看一下我自己,也深有体会。】我们正式在一起工作的是在2015年,当时我正加班完准备下班回家,就接到了王贤打来的一通电话【最近离职了,再找工作】。当时我也没多想,【毕竟王贤为人不错,肯学习肯努力肯干】就跟他说:“最近我们金服在招人,你可以来试试”。【面试的历程还是比较艰辛的,毕竟学历和技术摆在那里】。最后还是运气好还是等到了金服的offer。在新的公司,王贤除了接触项目上的一些事情外,也慢慢承担了一些项目沟通的工作。王贤自己知道自己的技术还是不行,需要学习的东西还有很多,他自己也明白,不努力、技术跟不上就会被淘汰。所以便每晚的加班到最后一个离开,也抽空余时间学习有关架构的相关技术点【购买了很多架构书籍,视频】。俗话说:“士别三日,则刮目相看”。不到一年的时间,王贤技术长进不少,大家都知道,这是他靠自己的辛勤汗水,每日每夜的加班熬出来的。“对我来说,如果工作有什么进步的诀窍的话,大概就是保持一颗刨根究底的心去做项目,就要孜孜不倦的学习新技术”,王贤如此总结自己能在工作中不断进步的经验。在今年18 年三月,王贤离开了金服,以50 万的年薪加入了目前势头最猛的某互联网公司,定级 P7 。又开始了新的征途。【技术过硬,还怕学历不行?】总结看了以上的经历以后,结合我个人的其他经历。我觉得,可以把这提炼成为三个关键字:学习,人脉,时间。三个关键字按照重要性从高到底排序,他们决定了一个架构师,甚至普通人的进阶的途径和方法。1:学习你可能觉得,以上的经历,很像流水账,貌似没有什么太出彩的地方。无非就是,跳跳槽,找找关系,去个牛叉的公司就行了。但是,仔细想想,好像没有这么简单吧。敢问:假设他肚子里面没有点墨水,即便有人推荐,也会有今天的成就么?假设没有对于未知事物的好奇心,他会跳出自己的舒适区,寻找新的挑战么?所以,永不倦怠的学习,才是成功的基石。甭管你在哪一个行业,别告诉自己学的都足够了,永远天外有天,人外有人。2:人脉这个不用多说,大家都明白,多认识朋友。以上的故事经历中,毫无疑问,他就是通过朋友,熟人介绍进入一家新公司。所以,朋友关系网是多么的重要。换句话说,我可以通过我现有的这些朋友,联系上名企中的任何一个人,你会发现,这太扯了,居然还能这样操作。同样,在人脉的背后,其实隐藏着另外一话题,就是所谓的情商。从人脉的角度来说情商,简单点来讲就是:如何做一个不让别人讨厌的人。只有不让人讨厌,大家相谈甚欢,才会有更深一层的了解,才会建立联系,最终成为同事,或者朋友,才会有人脉。3:时间下面有这么一个公式,可能有些朋友曾经见过。它告诉你,若每天比前一天进步0.01,非常微小的进步。但是一年累积下来,你会比一年前的你牛叉37.8倍。那十年呢,二十年呢?其实,这就是时间的力量。结尾最后,送大家一句话,我是在某个网站上看到的:再牛 x 的梦想也抵不住傻 x 似的坚持!还有,别走。我没有办法助你成功,那是洗脑工程师做的事儿。我倒是有这么个晋升渠道,它可能会帮你完成那每天的0.01的积累。以上这些技术如何学习?有没有免费的学习资料 ?点击右边此链接免费获取:https://jq.qq.com/?_wv=1027&k…

November 2, 2018 · 1 min · jiezi

【整理总结】负载均衡浅析

运营研发团队 施洪宝一. 基础知识1.1 基础什么是负载均衡?当单机提供的并发量不能满足需求时,我们需要多台服务器同时服务。当客户请求到达时,如何为客户选择最合适的服务器?这个问题就是负载均衡问题。负载均衡主要需要解决的问题是哪些?从客户端的角度上看,客户需要最快速的得到服务器的相应,负载均衡时需要找出能最快相应客户需求的服务器进行服务。从服务端来看如何使得每台服务器都能达到较高的利用率,最大限制的为用户提供快速、可靠的服务是服务端需要考虑的主要问题。1.2 负载均衡分类硬件F5软件dns负载均衡LVS负载均衡(4层)nginx, haproxy(7层)二. F5负载均衡F5是一家美国的公司,该公司生产一些硬件设备可以作为负载均衡器使用(例如:big-ip), 本文后续部分所说的F5是指其负载均衡器产品。不同的产品实现的功能不一致,具体情况需要根据产品说明书。F5可以在4-7层内做负载均衡,用户可以根据需求进行配置。由于F5可以做7层负载均衡,故而可以实现会话管理,http处理等。2.1 数据转发模式standard类型, 这种模式下,客户端与F5服务器建立连接,F5服务器与真实服务器建立连接,F5服务器将客户需求转发给真实服务器,并将真实服务器的相应转发给客户端,此时F5可以查看请求和相应的所有信息。四层转发模式(performance L4), 这种模式下,F5只处理4层以下的数据。客户端将数据发送给F5, F5仅将数据转发给真实服务器,包括TCP的握手数据包以及挥手数据包,真实服务器需要先将数据发送给F5服务器,F5将其转发给客户端。路由模式, 这种模式与LVS的DR模式类似。…2.2 负载均衡算法轮询,加权轮询。源地址哈希…2.3 小结F5的优势在于功能强大,并发量高,能满足客户的大多数需求,但其成本较高,一般大型国企可能会使用。2.4 参考https://f5.com/zhhttps://www.jianshu.com/p/2b5…https://wenku.baidu.com/view/…三. dns负载均衡dns负载均衡由dns服务提供厂商提供。最初的dns负载均衡提供简单轮询,不能根据客户端或者服务端状态进行选择。目前,有些dns服务厂商可以提供智能dns服务,用户可以设置负载均衡方案,例如:根据客户端ip地址,选择就近的服务器。对于目前大多数的公司而言,为了更好的服务用户,通常会使用dns负载均衡,将用户按照就近原则,分配到某个集群服务器上。之后,集群内再采用其他的负载均衡方案。四. Linux Virtual Server(LVS)LVS通过修改数据包Ip地址,Mac地址实现负载均衡。LVS由ipvs(内核中), ipvsadm(用户态)组成。LVS需要理解tcp,ip头部。当tcp握手信号,SYN数据包达到时,ipvs选择一个后端服务器,将数据包进行转发。在此之后,所有包含相同的ip,tcp头部的数据包都会被转发到之前选择的服务器上。很明显,ipvs无法感知数据包内容。4.1 分类LVS-NATLVS-DRLVS-TUN4.2 基本原理4.2.1 LVS-DRLVS-DR模式的基本原理如下图所示:4.2.2 LVS-NATLVS-NAT模式的基本原理如下图所示:4.3 负载均衡算法4.3.1 静态算法轮询(Round Robin, RR)加权轮询(Weight Round Robin, WRR)源地址Hash(Source Hash, SH)目的地址Hash(Destination Hash, DH), 可以设置多个VIP4.3.2 动态算法最少连接(Least Connections, LC),找出当前连接数最小的服务器加权最少连接(Weighted Least Connections, WLC)最短期望延迟(Shortest Expected Delay Scheduling, SED) 基于WLC。例如: 现有A, B, C三台服务器,权重分别为100,200,300,当前的连接数分别为1,2,3,下一个连接到达时,通过计算期望时延选择服务器(1+1)/100, (2+1)/200, (3+1)/300, 故而选择C服务器。永不排队(Never Queue Scheduling, NQ), 改进的sed, 如果某台服务器连接数为0,直接连接过去,不在进行sed计算。基于局部性的最少连接(locality-Based Least Connections, LBLC),根据目标ip, 找出目标ip最近使用的服务器,如果服务器存在并且负载没有大于一个阈值,则将新的连接分配到这个服务器上,否则按照最少连接找出一个服务器处理该请求。带复制的基于局部性最少连接(Locality-Based Least Connections with Replication, LBLCR),根据目标ip,维护一个服务器组,每次从组中挑选服务器,如果服务器不可以处理,则从所有服务器中按照最少连接挑选出一台服务器,并将其加入到目标ip的处理组服务器中。4.3 参考https://liangshuang.name/2017…五. Nginx Load Balancenginx负载均衡工作在7层,它会与client、upstream分别建立tcp连接,nginx需要维护这两个连接的状态。nginx的stream模块可以用于4层负载均衡,但一般很少使用。5.1 基本原理nginx做7层负载均衡的基本原理如下图所示:5.2 负载均衡算法轮询(默认)加权轮询源ip哈希响应时间url 哈希 ...

November 2, 2018 · 1 min · jiezi

【Nginx源码研究】内存管理部分

运营研发团队 施洪宝一. 概述应用程序的内存可以简单分为堆内存,栈内存。对于栈内存而言,在函数编译时,编译器会插入移动栈当前指针位置的代码,实现栈空间的自管理。而对于堆内存,通常需要程序员进行管理。我们通常说的内存管理亦是只堆空间内存管理。对于内存,我们的使用可以简化为3步,申请内存、使用内存、释放内存。申请内存,使用内存通常需要程序员显示操作,释放内存却并不一定需要程序员显示操作,目前很多的高级语言提供了垃圾回收机制,可以自行选择时机释放内存,例如: Go、Java已经实现垃圾回收, C语言目前尚未实现垃圾回收,C++中可以通过智能指针达到垃圾回收的目的。除了语言层面的内存管理外,有时我们需要在程序中自行管理内存,总体而言,对于内存管理,我认为主要是解决以下问题:用户申请内存时,如何快速查找到满足用户需求的内存块?用户释放内存时,如何避免内存碎片化?无论是语言层面实现的内存管理还是应用程序自行实现的内存管理,大都将内存按照大小分为几种,每种采用不同的管理模式。常见的分类是按照2的整数次幂分,将不同种类的内存通过链表链接,查询时,从相应大小的链表中寻找,如果找不到,则可以考虑从更大块内存中,拿取一块,将其分为多个小点的内存。当然,对于特别大的内存,语言层面的内存管理可以直接调用内存管理相关的系统调用,应用层面的内存管理则可以直接使用语言层面的内存管理。nginx内存管理整体可以分为2个部分,第一部分是常规的内存池,用于进程平时所需的内存管理;第二部分是共享内存的管理。总体而言,共享内存教内存池要复杂的多。二. nginx内存池管理2.1 说明本部分使用的nginx版本为1.15.3具体源码参见src/core/ngx_palloc.c文件2.2 nginx实现2.2.1 使用流程nginx内存池的使用较为简单,可以分为3步,调用ngx_create_pool函数获取ngx_pool_t指针。//size代表ngx_pool_t一块的大小ngx_pool_t* ngx_create_pool(size_t size, ngx_log_t log)调用ngx_palloc申请内存使用//从pool中申请size大小的内存void ngx_palloc(ngx_pool_t *pool, size_t size)释放内存(可以释放大块内存或者释放整个内存池)//释放从pool中申请的大块内存ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p)//释放整个内存池void ngx_destroy_pool(ngx_pool_t *pool)2.2.2 具体实现如下图所示,nginx将内存分为2种,一种是小内存,一种是大内存,当申请的空间大于pool->max时,我们认为是大内存空间,否则是小内存空间。//创建内存池的参数size减去头部管理结构ngx_pool_t的大小pool->max = size - sizeof(ngx_pool_t);对于小块内存空间, nginx首先查看当前内存块待分配的空间中,是否能够满足用户需求,如果可以,则直接将这部分内存返回。如果不能满足用户需求,则需要重新申请一个内存块,申请的内存块与当前块空间大小相同,将新申请的内存块通过链表链接到上一个内存块,从新的内存块中分配用户所需的内存。小块内存并不释放,用户申请后直接使用,即使后期不再使用也不需要释放该内存。由于用户有时并不知道自己使用的内存块是大是小,此时也可以调用ngx_pfree函数释放该空间,该函数会从大空间链表中查找内存,找到则释放内存。对于小内存而言,并未做任何处理。对于大块内存, nginx会将这些内存放到链表中存储,通过pool->large进行管理。值得注意的是,用户管理大内存的ngx_pool_large_t结构是从本内存池的小块内存中申请而来,也就意味着无法释放这些内存,nginx则是直接复用ngx_pool_large_t结构体。当用户需要申请大内存空间时,利用c函数库malloc申请空间,然后将其挂载某个ngx_pool_large_t结构体上。nginx在需要一个新的ngx_pool_large_t结构时,会首先pool->large链表的前3个元素中,查看是否有可用的,如果有则直接使用,否则新建ngx_pool_large_t结构。三. nginx共享内存管理3.1 说明本部分使用的nginx版本是1.15.3本部分源码详见src/core/ngx_slab.c, src/core/ngx_shmtx.cnginx共享内存内容相对较多,本文仅做简单概述。3.2 直接使用共享内存3.2.1 基础nginx中需要创建互斥锁,用于后面多进程同步使用。除此之外,nginx可能需要一些统计信息,例如设置(stat_stub),对于这些变量,我们并不需要特意管理,只需要开辟共享空间后,直接使用即可。设置stat_stub后所需的统计信息,亦是放到共享内存中,我们此处仅以nginx中的互斥锁进行说明。3.2.2 nginx互斥锁的实现nginx互斥锁,有两种方案,当系统支持原子操作时,采用原子操作,不支持时采用文件锁。本节源码见ngx_event_module_init函数。下图为文件锁实现互斥锁的示意图。下图为原子操作实现互斥锁的示意图。问题reload时,新启动的master向老的master发送信号后直接退出,旧的master,重新加载配置(ngx_init_cycle函数), 新创建工作进程, 新的工作进程与旧的工作进程使用的锁是相同的。平滑升级时, 旧的master会创建新的master, 新的master会继承旧的master监听的端口(通过环境变量传递监听套接字对应的fd),新的进程并没有重新绑定监听端口。可能存在新老worker同时监听某个端口的情况,此时操作系统会保证只会有一个进程处理该事件(虽然epoll_wait都会被唤醒)。3.3 通过slab管理共享内存nginx允许各个模块开辟共享空间以供使用,例如ngx_http_limit_conn_module模块。nginx共享内存管理的基本思想有:将内存按照页进行分配,每页的大小相同, 此处设为page_size。将内存块按照2的整数次幂进行划分, 最小为8bit, 最大为page_size/2。例如,假设每页大小为4Kb, 则将内存分为8, 16, 32, 64, 128, 256, 512, 1024, 2048共9种,每种对应一个slot, 此时slots数组的大小n即为9。申请小块内存(申请内存大小size <= page_size/2)时,直接给用户这9种中的一种,例如,需要30bit时,找大小为32的内存块提供给用户。每个页只会划分一种类型的内存块。例如,某次申请内存时,现有内存无法满足要求,此时会使用一个新的页,则这个新页此后只会分配这种大小的内存。通过双向链表将所有空闲的页连接。图中ngx_slab_pool_t中的free变量即使用来链接空闲页的。通过slots数组将所有小块内存所使用的页链接起来。对于大于等于页面大小的空间请求,计算所需页数,找到连续的空闲页,将空闲页的首页地址返回给客户使用,通过每页的管理结构ngx_slab_page_t进行标识。所有页面只会有3中状态,空闲、未满、已满。空闲,未满都是通过双向链表进行整合,已满页面则不存在与任何页面,当空间被释放时,会将其加入到某个链表。nginx共享内存的基本结构图如下:在上图中,除了最右侧的ngx_slab_pool_t接口开始的一段内存位于共享内存区外,其他内存都不是共享内存。共享内存最终是从page中分配而来。

October 31, 2018 · 1 min · jiezi

【Nginx源码研究】Nginx的事件模块介绍

运营研发团队 谭淼一、nginx模块介绍高并发是nginx最大的优势之一,而高并发的原因就是nginx强大的事件模块。本文将重点介绍nginx是如果利用Linux系统的epoll来完成高并发的。首先介绍nginx的模块,nginx1.15.5源码中,自带的模块主要分为core模块、conf模块、event模块、http模块和mail模块五大类。其中mail模块比较特殊,本文暂不讨论。查看nginx模块属于哪一类也很简单,对于每一个模块,都有一个ngx_module_t类型的结构体,该结构体的type字段就是标明该模块是属于哪一类模块的。以ngx_http_module为例:ngx_module_t ngx_http_module = { NGX_MODULE_V1, &ngx_http_module_ctx, /* module context / ngx_http_commands, / module directives / NGX_CORE_MODULE, / module type / NULL, / init master / NULL, / init module / NULL, / init process / NULL, / init thread / NULL, / exit thread / NULL, / exit process / NULL, / exit master */ NGX_MODULE_V1_PADDING};可以ngx_core_module是属于NGX_CORE_MODULE类型的模块。由于本文主要介绍使用epoll来完成nginx的事件驱动,故主要介绍core模块的ngx_events_module与event模块的ngx_event_core_module、ngx_epoll_module。二、epoll介绍2.1、epoll原理关于epoll的实现原理,本文不会具体介绍,这里只是介绍epoll的工作流程。具体的实现参考:https://titenwang.github.io/2…epoll的使用是三个函数:int epoll_create(int size);int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);首先epoll_create函数会在内核中创建一块独立的内存存储一个eventpoll结构体,该结构体包括一颗红黑树和一个链表,如下图所示:然后通过epoll_ctl函数,可以完成两件事。(1)将事件添加到红黑树中,这样可以防止重复添加事件;(2)将事件与网卡建立回调关系,当事件发生时,网卡驱动会回调ep_poll_callback函数,将事件添加到epoll_create创建的链表中。最后,通过epoll_wait函数,检查并返回链表中是否有事件。该函数是阻塞函数,阻塞时间为timeout,当双向链表有事件或者超时的时候就会返回链表长度(发生事件的数量)。2.2、epoll相关函数的参数(1)epoll_create函数的参数size表示该红黑树的大致数量,实际上很多操作系统没有使用这个参数。(2)epoll_ctl函数的参数为epfd,op,fd和event。(3)epoll_wait函数的参数为epfd,events,maxevents和timeout三、事件模块的初始化众所周知,nginx是master/worker框架,在nginx启动时是一个进程,在启动的过程中master会fork出了多个子进程作为worker。master主要是管理worker,本身并不处理请求。而worker负责处理请求。因此,事件模块的初始化也是分成两部分。一部分发生在fork出worker前,主要是配置文件解析等操作,另外一部分发生在fork之后,主要是向epoll中添加监听事件。3.1 启动进程对事件模块的初始化启动进程对事件模块的初始化分为配置文件解析、开始监听端口和ngx_event_core_module模块的初始化。这三个步骤均在ngx_init_cycle函数进行。调用关系:main() —> ngx_init_cycle()下图是ngx_init_cycle函数的流程,红框是本节将要介绍的三部分内容。3.1.1 配置文件解析启动进程的一个主要工作是解析配置文件。在nginx中,用户主要通过nginx配置文件nginx.conf的event块来控制和调节事件模块的参数。下面是一个event块配置的示例:user nobody;worker_processes 1;error_log logs/error.log;pid logs/nginx.pid;…… events { use epoll; worker_connections 1024; accept_mutex on;} http { ……}首先我们先看看nginx是如何解析event块,并将event块存储在什么地方。在nginx中,解析配置文件的工作是调用ngx_init_cycle函数完成的。下图是该函数在解析配置文件部分的一个流程:(1)ngx_init_cycle函数首先会进行一些初始化工作,包括更新时间,创建内存池和创建并更新ngx_cycle_t结构体cycle;(2)调用各个core模块的create_conf方法,可以创建cycle的conf_ctx数组,该阶段完成后cycle->conf_ctx如下图所示:(3)初始化ngx_conf_t类型的结构体conf,将cycle->conf_ctx结构体赋值给conf的ctx字段(4)解析配置文件解析配置文件会调用ngx_conf_parse函数,该函数会解析一行命令,当遇到块时会递归调用自身。解析的方法也很简单,就是读取一个命令,然后在所有模块的cmd数组中寻找该命令,若找到则调用该命令的cmd->set(),完成参数的解析。下面介绍event块的解析。event命令是在event/ngx_event.c文件中定义的,代码如下。static ngx_command_t ngx_events_commands[] = { { ngx_string(“events”), NGX_MAIN_CONF|NGX_CONF_BLOCK|NGX_CONF_NOARGS, ngx_events_block, 0, 0, NULL }, ngx_null_command};在从配置文件中读取到event后,会调用ngx_events_block函数。下面是ngx_events_block函数的主要工作:解析完配置文件中的event块后,cycle->conf_ctx如下图所示:(5)解析完整个配置文件后,调用各个core类型模块的init_conf方法。ngx_event_module的ctx的init_conf方法为ngx_event_init_conf。该方法并没有实际的用途,暂不详述。3.1.2 监听socket虽然监听socket和事件模块并没有太多的关系,但是为了使得整个流程完整,此处会简单介绍一下启动进程是如何监听端口的。该过程首先检查old_cycle,如果old_cycle中有和cycle中相同的socket,就直接把old_cycle中的fd赋值给cycle。之后会调用ngx_open_listening_socket函数,监听端口。下面是ngx_open_listening_sockets函数,该函数的作用是遍历所有需要监听的端口,然后调用socket(),bind()和listen()函数,该函数会重试5次。ngx_int_tngx_open_listening_sockets(ngx_cycle_t cycle){ …… / 重试5次 / for (tries = 5; tries; tries–) { failed = 0; / 遍历需要监听的端口 / ls = cycle->listening.elts; for (i = 0; i < cycle->listening.nelts; i++) { …… / ngx_socket函数就是socket函数 / s = ngx_socket(ls[i].sockaddr->sa_family, ls[i].type, 0); …… / 设置socket属性 */ if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (const void ) &reuseaddr, sizeof(int)) == -1) { …… } …… / IOCP事件操作 / if (!(ngx_event_flags & NGX_USE_IOCP_EVENT)) { if (ngx_nonblocking(s) == -1) { …… } } …… / 绑定socket和地址 / if (bind(s, ls[i].sockaddr, ls[i].socklen) == -1) { …… } …… / 开始监听 / if (listen(s, ls[i].backlog) == -1) { …… } ls[i].listen = 1; ls[i].fd = s; } …… / 两次重试间隔500ms */ ngx_msleep(500); } …… return NGX_OK;}3.1.3 ngx_event_core_module模块的初始化在ngx_init_cycle函数监听完端口,并提交新的cycle后,便会调用ngx_init_modules函数,该方法会遍历所有模块并调用其init_module方法。对于该阶段,和事件驱动模块有关系的只有ngx_event_core_module的ngx_event_module_init方法。该方法主要做了下面三个工作:(1)获取core模块配置结构体中的时间精度timer_resolution,用在epoll里更新缓存时间(2)调用getrlimit方法,检查连接数是否超过系统的资源限制(3)利用 mmap 分配一块共享内存,存储负载均衡锁(ngx_accept_mutex)、连接计数器(ngx_connection_counter)3.2 worker进程对事件模块的初始化启动进程在完成一系列操作后,会fork出master进程,并自我关闭,让master进程继续完成初始化工作。master进程会在ngx_spawn_process函数中fork出worker进程,并让worker进程调用ngx_worker_process_cycle函数。ngx_worker_process_cycle函数是worker进程的主循环函数,该函数首先会调用ngx_worker_process_init函数完成worker的初始化,然后就会进入到一个循环中,持续监听处理请求。事件模块的初始化就发生在ngx_worker_process_init函数中。其调用关系:main() —> ngx_master_process_cycle() —> ngx_start_worker_processes() —> ngx_spawn_process() —> ngx_worker_process_cycle() —> ngx_worker_process_init()。对于ngx_worker_process_init函数,会调用各个模块的init_process方法:static voidngx_worker_process_init(ngx_cycle_t cycle, ngx_int_t worker){ …… for (i = 0; cycle->modules[i]; i++) { if (cycle->modules[i]->init_process) { if (cycle->modules[i]->init_process(cycle) == NGX_ERROR) { / fatal / exit(2); } } } ……}在此处,会调用ngx_event_core_module的ngx_event_process_init函数。该函数较为关键,将会重点解析。在介绍ngx_event_process_init函数前,先介绍两个终于的结构体,由于这两个结构体较为复杂,故只介绍部分字段:(1)ngx_event_s结构体。nginx中,事件会使用ngx_event_s结构体来表示。ngx_event_sstruct ngx_event_s { / 通常指向ngx_connection_t结构体 */ void data; / 事件可写 / unsigned write:1; / 事件可建立新连接 / unsigned accept:1; / 检测事件是否过期 / unsigned instance:1; / 通常将事件加入到epoll中会将该字段置为1 / unsigned active:1; …… / 事件超时 / unsigned timedout:1; / 事件是否在定时器中 / unsigned timer_set:1; …… / 事件是否在延迟处理队列中 / unsigned posted:1; …… / 事件的处理函数 / ngx_event_handler_pt handler; …… / 定时器红黑树节点 / ngx_rbtree_node_t timer; / 延迟处理队列节点 / ngx_queue_t queue; ……};(2)ngx_connection_s结构体代表一个nginx连接struct ngx_connection_s { / 若该结构体未使用,则指向下一个为使用的ngx_connection_s,若已使用,则指向ngx_http_request_t */ void data; / 指向一个读事件结构体,这个读事件结构体表示该连接的读事件 */ ngx_event_t read; / 指向一个写事件结构体,这个写事件结构体表示该连接的写事件 */ ngx_event_t write; / 连接的套接字 / ngx_socket_t fd; …… / 该连接对应的监听端口,表示是由该端口建立的连接 */ ngx_listening_t listening; ……};下面介绍ngx_event_process_init函数的实现,代码如下:/ 此方法在worker进程初始化时调用 /static ngx_int_tngx_event_process_init(ngx_cycle_t cycle){ …… / 打开accept_mutex负载均衡锁,用于防止惊群 / if (ccf->master && ccf->worker_processes > 1 && ecf->accept_mutex) { ngx_use_accept_mutex = 1; ngx_accept_mutex_held = 0; ngx_accept_mutex_delay = ecf->accept_mutex_delay; } else { ngx_use_accept_mutex = 0; } / 初始化两个队列,一个用于存放不能及时处理的建立连接事件,一个用于存储不能及时处理的读写事件 / ngx_queue_init(&ngx_posted_accept_events); ngx_queue_init(&ngx_posted_events); / 初始化定时器 / if (ngx_event_timer_init(cycle->log) == NGX_ERROR) { return NGX_ERROR; } / * 调用使用的ngx_epoll_module的ctx的actions的init方法,即ngx_epoll_init函数 * 该函数主要的作用是调用epoll_create()和创建用于epoll_wait()返回事件链表的event_list */ for (m = 0; cycle->modules[m]; m++) { …… if (module->actions.init(cycle, ngx_timer_resolution) != NGX_OK) { exit(2); } break; } / 如果在配置中设置了timer_resolution,则要设置控制时间精度。通过setitimer方法会设置一个定时器,每隔timer_resolution的时间会发送一个SIGALRM信号 / if (ngx_timer_resolution && !(ngx_event_flags & NGX_USE_TIMER_EVENT)) { …… sa.sa_handler = ngx_timer_signal_handler; sigemptyset(&sa.sa_mask); if (sigaction(SIGALRM, &sa, NULL) == -1) { …… } itv.it_interval.tv_sec = ngx_timer_resolution / 1000; …… if (setitimer(ITIMER_REAL, &itv, NULL) == -1) { …… } } …… / 分配连接池空间 / cycle->connections = ngx_alloc(sizeof(ngx_connection_t) * cycle->connection_n, cycle->log); …… c = cycle->connections; / 分配读事件结构体数组空间,并初始化读事件的closed和instance / cycle->read_events = ngx_alloc(sizeof(ngx_event_t) * cycle->connection_n, cycle->log); …… rev = cycle->read_events; for (i = 0; i < cycle->connection_n; i++) { rev[i].closed = 1; rev[i].instance = 1; } / 分配写事件结构体数组空间,并初始化写事件的closed / cycle->write_events = ngx_alloc(sizeof(ngx_event_t) * cycle->connection_n, cycle->log); …… wev = cycle->write_events; for (i = 0; i < cycle->connection_n; i++) { wev[i].closed = 1; } / 将序号为i的读事件结构体和写事件结构体赋值给序号为i的connections结构体的元素 / i = cycle->connection_n; next = NULL; do { i–; / 将connection的data字段设置为下一个connection / c[i].data = next; c[i].read = &cycle->read_events[i]; c[i].write = &cycle->write_events[i]; c[i].fd = (ngx_socket_t) -1; next = &c[i]; } while (i); / 初始化cycle->free_connections / cycle->free_connections = next; cycle->free_connection_n = cycle->connection_n; / 为每个监听端口分配连接 / ls = cycle->listening.elts; for (i = 0; i < cycle->listening.nelts; i++) { …… c = ngx_get_connection(ls[i].fd, cycle->log); …… rev = c->read; …… / 为监听的端口的connection结构体的read事件设置回调函数 / rev->handler = (c->type == SOCK_STREAM) ? ngx_event_accept : ngx_event_recvmsg; / 将监听的connection的read事件添加到事件驱动模块(epoll) */ …… if (ngx_add_event(rev, NGX_READ_EVENT, 0) == NGX_ERROR) { return NGX_ERROR; } } return NGX_OK;}该方法主要做了下面几件事:(1)打开accept_mutex负载均衡锁,用于防止惊群。惊群是指当多个worker都处于等待事件状态,如果突然来了一个请求,就会同时唤醒多个worker,但是只有一个worker会处理该请求,这就造成系统资源浪费。为了解决这个问题,nginx使用了accept_mutex负载均衡锁。各个worker首先会抢锁,抢到锁的worker才会监听各个端口。(2)初始化两个队列,一个用于存放不能及时处理的建立连接事件,一个用于存储不能及时处理的读写事件。(3)初始化定时器,该定时器就是一颗红黑树,根据时间对事件进行排序。(4)调用使用的ngx_epoll_module的ctx的actions的init方法,即ngx_epoll_init函数。该函数较为简单,主要的作用是调用epoll_create()和创建用于存储epoll_wait()返回事件的链表event_list。(5)如果再配置中设置了timer_resolution,则要设置控制时间精度,用于控制nginx时间。这部分在第五部分重点讲解。(6)分配连接池空间、读事件结构体数组、写事件结构体数组。上文介绍了ngx_connection_s和ngx_event_s结构体,我们了解到每一个ngx_connection_s结构体都有两个ngx_event_s结构体,一个读事件,一个写事件。在这个阶段,会向内存池中申请三个数组:cycle->connections、cycle->read_events和cycle->write_events,并将序号为i的读事件结构体和写事件结构体赋值给序号为i的connections结构体的元素。并将cycle->free_connections指向第一个未使用的ngx_connections结构体。(7)为每个监听端口分配连接在此阶段,会获取cycle->listening数组中的ngx_listening_s结构体元素。在3.1.2小节中,我们已经讲了nginx启动进程会监听端口,并将socket连接的fd存储在cycle->listening数组中。在这里,会获取到3.1.2小节中监听的端口,并为每个监听分配连接结构体。(8)为每个监听端口的连接的读事件设置handler在为cycle->listening的元素分配完ngx_connection_s类型的连接后,会为连接的读事件设置回调方法handler。这里handler为ngx_event_accept函数,对于该函数,将在后文讲解。(9)将每个监听端口的连接的读事件添加到epoll中在此处,会调用ngx_epoll_module的ngx_epoll_add_event函数,将监听端口的连接的读事件(ls[i].connection->read)添加到epoll中。ngx_epoll_add_event函数的流程如下:在向epoll中添加事件前,需要判断之前是否添加过该连接的事件。 至此,ngx_event_process_init的工作完成,事件模块的初始化也完成了。后面worker开始进入循环监听阶段。四、事件处理4.1 worker的主循环函数ngx_worker_process_cycleworker在初始化完成之后,开始循环监听端口,并处理请求。下面开始我们开始讲解worker是如何处理事件的。worker的循环代码如下:static voidngx_worker_process_cycle(ngx_cycle_t *cycle, void data){ ngx_int_t worker = (intptr_t) data; ngx_process = NGX_PROCESS_WORKER; ngx_worker = worker; / 初始化worker / ngx_worker_process_init(cycle, worker); ngx_setproctitle(“worker process”); for ( ;; ) { if (ngx_exiting) { …… } ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0, “worker cycle”); / 处理IO事件和时间事件 / ngx_process_events_and_timers(cycle); if (ngx_terminate) { …… } if (ngx_quit) { …… } if (ngx_reopen) { …… } }}可以看到,在worker初始化后进入一个for循环,所有的IO事件和时间事件都是在函数ngx_process_events_and_timers中处理的。4.2 worker的事件处理函数ngx_process_events_and_timers在worker的主循环中,所有的事件都是通过函数ngx_process_events_and_timers处理的,该函数的代码如下:/ 事件处理函数和定时器处理函数 /voidngx_process_events_and_timers(ngx_cycle_t cycle){ ngx_uint_t flags; ngx_msec_t timer, delta; / timer_resolution模式,设置epoll_wait函数阻塞ngx_timer_resolution的时间 / if (ngx_timer_resolution) { / timer_resolution模式 / timer = NGX_TIMER_INFINITE; flags = 0; } else { / 非timer_resolution模式,epoll_wait函数等待至下一个定时器事件到来时返回 / timer = ngx_event_find_timer(); flags = NGX_UPDATE_TIME; } / 是否使用accept_mutex / if (ngx_use_accept_mutex) { / * 该worker是否负载过高,若负载过高则不抢锁 * 判断负载过高是判断该worker建立的连接数是否大于该worker可以建立的最大连接数的7/8 / if (ngx_accept_disabled > 0) { ngx_accept_disabled–; } else { / 抢锁 / if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) { return; } if (ngx_accept_mutex_held) { / 抢到锁,则收到事件后暂不处理,先扔到事件队列中 / flags |= NGX_POST_EVENTS; } else { / 未抢到锁,要修改worker在epoll_wait函数等待的时间,使其不要过大 / if (timer == NGX_TIMER_INFINITE || timer > ngx_accept_mutex_delay) { timer = ngx_accept_mutex_delay; } } } } / delta用于计算ngx_process_events的耗时 / delta = ngx_current_msec; / 事件处理函数,epoll使用的是ngx_epoll_process_events函数 / (void) ngx_process_events(cycle, timer, flags); delta = ngx_current_msec - delta; ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0, “timer delta: %M”, delta); / 处理ngx_posted_accept_events队列的连接事件 / ngx_event_process_posted(cycle, &ngx_posted_accept_events); / 若持有accept_mutex,则释放锁 / if (ngx_accept_mutex_held) { ngx_shmtx_unlock(&ngx_accept_mutex); } / 若事件处理函数的执行时间不为0,则要处理定时器事件 / if (delta) { ngx_event_expire_timers(); } / 处理ngx_posted_events队列的读写事件 / ngx_event_process_posted(cycle, &ngx_posted_events);}ngx_process_events_and_timers函数是nginx处理事件的核心函数,主要的工作可以分为下面几部分:(1)设置nginx更新时间的方式。nginx会将时间存储在内存中,每隔一段时间调用ngx_time_update函数更新时间。那么多久更新一次呢?nginx提供两种方式:方式一:timer_resolution模式。在nginx配置文件中,可以使用timer_resolution之类来选择此方式。如果使用此方式,会将epoll_wait的阻塞时间设置为无穷大,即一直阻塞。那么如果nginx一直都没有收到事件,会一直阻塞吗?答案是不会的。在本文3.2节中讲解的ngx_event_process_init函数(第5步)将会设置一个时间定时器和一个信号处理函数,其中时间定时器会每隔timer_resolution的时间发送一个SIGALRM信号,而当worker收到时间定时器发送的信号,会将epoll_wait函数终端,同时调用SIGALRM信号的中断处理函数,将全局变量ngx_event_timer_alarm置为1。后面会检查该变量,调用ngx_time_update函数来更新nginx的时间。方式二:如果不在配置文件中设置timer_resolution,nginx默认会使用方式二来更新nginx的时间。首先会调用ngx_event_find_timer函数来设置epoll_wait的阻塞时间,ngx_event_find_timer函数返回的是下一个时间事件发生的时间与当前时间的差值,即让epoll_wait阻塞到下一个时间事件发生为止。当使用这种模式,每当epoll_wait返回,都会调用ngx_time_update函数更新时间。(2)使用负载均衡锁ngx_use_accept_mutex。上文曾经提过一个问题,当多个worker都处于等待事件状态,如果突然来了一个请求,就会同时唤醒多个worker,但是只有一个worker会处理该请求,这就造成系统资源浪费。nginx如果解决这个问题呢?答案就是使用一个锁来解决。在监听事件前,各个worker会进行一次抢锁行为,只有抢到锁的worker才会监听端口,而其他worker值处理已经建立连接的事件。首先函数会通过ngx_accept_disabled是否大于0来判断是否过载,过载的worker是不允许抢锁的。ngx_accept_disabled的计算方式如下。/ * ngx_cycle->connection_n是每个进程最大连接数,也是连接池的总连接数,ngx_cycle->free_connection_n是连接池中未使用的连接数量。 * 当未使用的数量小于总数量的1/8时,会使ngx_accept_disabled大于0。这时认为该worker过载。 **/ngx_accept_disabled = ngx_cycle->connection_n / 8 - ngx_cycle->free_connection_n;若ngx_accept_disabled小于0,worker可以抢锁。这时会通过ngx_trylock_accept_mutex函数抢锁。该函数的流程如下图所示:在抢锁结束后,若worker抢到锁,设置该worker的flag为NGX_POST_EVENTS,表示抢到锁的这个worker在收到事件后并不会立即调用事件的处理函数,而是会把事件放到一个队列里,后期处理。(3)调用事件处理函数ngx_process_events,epoll使用的是ngx_epoll_process_events函数。此代码较为重要,下面是代码:static ngx_int_tngx_epoll_process_events(ngx_cycle_t *cycle, ngx_msec_t timer, ngx_uint_t flags){ int events; uint32_t revents; ngx_int_t instance, i; ngx_uint_t level; ngx_err_t err; ngx_event_t *rev, *wev; ngx_queue_t queue; ngx_connection_t c; ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0, “epoll timer: %M”, timer); / 调用epoll_wait,从epoll中获取发生的事件 / events = epoll_wait(ep, event_list, (int) nevents, timer); err = (events == -1) ? ngx_errno : 0; / 两种方式更新nginx时间,timer_resolution模式ngx_event_timer_alarm为1,非timer_resolution模式flags & NGX_UPDATE_TIME不为0,均会进入if条件 / if (flags & NGX_UPDATE_TIME || ngx_event_timer_alarm) { ngx_time_update(); } / 处理epoll_wait返回为-1的情况 / if (err) { / * 对于timer_resolution模式,如果worker接收到SIGALRM信号,会调用该信号的处理函数,将ngx_event_timer_alarm置为1,从而更新时间。 * 同时如果在epoll_wait阻塞的过程中接收到SIGALRM信号,会中断epoll_wait,使其返回NGX_EINTR。由于上一步已经更新了时间,这里要把ngx_event_timer_alarm置为0。 */ if (err == NGX_EINTR) { if (ngx_event_timer_alarm) { ngx_event_timer_alarm = 0; return NGX_OK; } level = NGX_LOG_INFO; } else { level = NGX_LOG_ALERT; } ngx_log_error(level, cycle->log, err, “epoll_wait() failed”); return NGX_ERROR; } / 若events返回为0,判断是因为epoll_wait超时还是其他原因 / if (events == 0) { if (timer != NGX_TIMER_INFINITE) { return NGX_OK; } ngx_log_error(NGX_LOG_ALERT, cycle->log, 0, “epoll_wait() returned no events without timeout”); return NGX_ERROR; } / 对epoll_wait返回的链表进行遍历 / for (i = 0; i < events; i++) { c = event_list[i].data.ptr; / 从data中获取connection & instance的值,并解析出instance和connection */ instance = (uintptr_t) c & 1; c = (ngx_connection_t ) ((uintptr_t) c & (uintptr_t) ~1); / 取出connection的read事件 / rev = c->read; / 判断读事件是否过期 / if (c->fd == -1 || rev->instance != instance) { ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0, “epoll: stale event %p”, c); continue; } / 取出事件的类型 / revents = event_list[i].events; ngx_log_debug3(NGX_LOG_DEBUG_EVENT, cycle->log, 0, “epoll: fd:%d ev:%04XD d:%p”, c->fd, revents, event_list[i].data.ptr); / 若连接发生错误,则将EPOLLIN、EPOLLOUT添加到revents中,在调用读写事件时能够处理连接的错误 / if (revents & (EPOLLERR|EPOLLHUP)) { ngx_log_debug2(NGX_LOG_DEBUG_EVENT, cycle->log, 0, “epoll_wait() error on fd:%d ev:%04XD”, c->fd, revents); revents |= EPOLLIN|EPOLLOUT; } / 事件为读事件且读事件在epoll中 / if ((revents & EPOLLIN) && rev->active) { #if (NGX_HAVE_EPOLLRDHUP) if (revents & EPOLLRDHUP) { rev->pending_eof = 1; } rev->available = 1;#endif rev->ready = 1; / 事件是否需要延迟处理?对于抢到锁监听端口的worker,会将事件延迟处理 / if (flags & NGX_POST_EVENTS) { / 根据事件的是否是accept事件,加到不同的队列中 / queue = rev->accept ? &ngx_posted_accept_events : &ngx_posted_events; ngx_post_event(rev, queue); } else { / 若不需要延迟处理,直接调用read事件的handler / rev->handler(rev); } } / 取出connection的write事件 / wev = c->write; / 事件为写事件且写事件在epoll中 / if ((revents & EPOLLOUT) && wev->active) { / 判断写事件是否过期 / if (c->fd == -1 || wev->instance != instance) { ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0, “epoll: stale event %p”, c); continue; } wev->ready = 1;#if (NGX_THREADS) wev->complete = 1;#endif / 事件是否需要延迟处理?对于抢到锁监听端口的worker,会将事件延迟处理 / if (flags & NGX_POST_EVENTS) { ngx_post_event(wev, &ngx_posted_events); } else { / 若不需要延迟处理,直接调用write事件的handler */ wev->handler(wev); } } } return NGX_OK;}该函数的流程图如下:(4)计算ngx_process_events函数的调用时间。(5)处理ngx_posted_accept_events队列的连接事件。这里就是遍历ngx_posted_accept_events队列,调用事件的handler方法,这里accept事件的handler为ngx_event_accept。(6)释放负载均衡锁。(7)处理定时器事件,具体操作是在定时器红黑树中查找过期的事件,调用其handler方法。(8)处理ngx_posted_events队列的读写事件,即遍历ngx_posted_events队列,调用事件的handler方法。结束至此,我们介绍完了nginx事件模块的事件处理函数ngx_process_events_and_timers。nginx事件模块的相关知识也初步介绍完了。 ...

October 30, 2018 · 6 min · jiezi

【Nginx模块编写】编写第一个Nginx模块

运营研发团队 季伟滨模块名:ngx_http_jiweibin_module1、建立模块源码目录mkdir /data/code/c/nginx-1.6.2/src/plugin2、新建config文件vim /data/code/c/nginx-1.6.2/src/plugin/config,写入如下配置:ngx_addon_name=ngx_http_jiweibin_moduleHTTP_MODULES="$HTTP_MODULES ngx_http_jiweibin_module"NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_jiweibin_module.c"3、新建ngx_http_jiweibin_module.c#include <ngx_config.h>#include <ngx_core.h>#include <ngx_http.h> static char *ngx_http_jiweibin_cmd_set(ngx_conf_t *cf,ngx_command_t *cmd,void *conf);static ngx_int_t ngx_http_jiweibin_handler(ngx_http_request_t *r); static ngx_command_t ngx_http_jiweibin_commands[] = { { ngx_string(“jiweibin”), NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_HTTP_LMT_CONF|NGX_CONF_NOARGS, ngx_http_jiweibin_cmd_set, NGX_HTTP_LOC_CONF_OFFSET, 0, NULL }, ngx_null_command }; static char * ngx_http_jiweibin_cmd_set(ngx_conf_t *cf,ngx_command_t *cmd,void *conf){ ngx_http_core_loc_conf_t *clcf; clcf = ngx_http_conf_get_module_loc_conf(cf,ngx_http_core_module); clcf->handler = ngx_http_jiweibin_handler; return NGX_CONF_OK;}static ngx_http_module_t ngx_http_jiweibin_module_ctx = { NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL}; ngx_module_t ngx_http_jiweibin_module = { NGX_MODULE_V1, &ngx_http_jiweibin_module_ctx, ngx_http_jiweibin_commands, NGX_HTTP_MODULE, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NGX_MODULE_V1_PADDING};static ngx_int_t ngx_http_jiweibin_handler(ngx_http_request_t *r){ if(!(r->method & (NGX_HTTP_GET|NGX_HTTP_POST))){ return NGX_HTTP_NOT_ALLOWED; } ngx_int_t rc = ngx_http_discard_request_body(r); if(rc != NGX_OK){ return rc; } ngx_str_t content_type = ngx_string(“text/plain”); ngx_str_t response = ngx_string(“hello world”); r->headers_out.status = NGX_HTTP_OK; r->headers_out.content_length_n = response.len; r->headers_out.content_type = content_type; rc = ngx_http_send_header(r); if(rc == NGX_ERROR || rc > NGX_OK){ return rc; } ngx_buf_t *b; b = ngx_create_temp_buf(r->pool,response.len); if(b == NULL){ return NGX_HTTP_INTERNAL_SERVER_ERROR; } ngx_memcpy(b->pos,response.data,response.len); b->last = b->pos + response.len; b->last_buf = 1; ngx_chain_t out; out.buf = b; out.next = NULL; return ngx_http_output_filter(r,&out);}4、configurecd /data/code/c/nginx-1.6.2./configure –prefix=/home/xiaoju/nginx-jiweibin –add-module=/data/code/c/nginx-1.6.2/src/plugin/5、make & make install6、配置nginx7、杀死旧的nginx进程8、启动新编译的带有插件的nginx/home/xiaoju/nginx-jiweibin/sbin/nginx -c /home/xiaoju/nginx-jiweibin/conf/nginx.conf9、验证自己写的插件http://10.179.195.72:8080/hello ...

October 30, 2018 · 1 min · jiezi

dubbo负载均衡策略及对应源码分析

在集群负载均衡时,Dubbo 提供了多种均衡策略,缺省为 random 随机调用。我们还可以扩展自己的负责均衡策略,前提是你已经从一个小白变成了大牛,嘻嘻1、Random LoadBalance1.1 随机,按权重设置随机概率。1.2 在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。1.3 源码分析package com.alibaba.dubbo.rpc.cluster.loadbalance;import java.util.List;import java.util.Random;import com.alibaba.dubbo.common.URL;import com.alibaba.dubbo.rpc.Invocation;import com.alibaba.dubbo.rpc.Invoker;/** * random load balance. * * @author qianlei * @author william.liangf /public class RandomLoadBalance extends AbstractLoadBalance {public static final String NAME = “random”;private final Random random = new Random(); protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) { int length = invokers.size(); // 总个数 int totalWeight = 0; // 总权重 boolean sameWeight = true; // 权重是否都一样 for (int i = 0; i < length; i++) { int weight = getWeight(invokers.get(i), invocation); totalWeight += weight; // 累计总权重 if (sameWeight && i > 0 && weight != getWeight(invokers.get(i - 1), invocation)) { sameWeight = false; // 计算所有权重是否一样 } } if (totalWeight > 0 && ! sameWeight) { // 如果权重不相同且权重大于0则按总权重数随机 int offset = random.nextInt(totalWeight); // 并确定随机值落在哪个片断上 for (int i = 0; i < length; i++) { offset -= getWeight(invokers.get(i), invocation); if (offset < 0) { return invokers.get(i); } } } // 如果权重相同或权重为0则均等随机 return invokers.get(random.nextInt(length));}}说明:从源码可以看出随机负载均衡的策略分为两种情况a. 如果总权重大于0并且权重不相同,就生成一个1totalWeight(总权重数)的随机数,然后再把随机数和所有的权重值一一相减得到一个新的随机数,直到随机 数小于0,那么此时访问的服务器就是使得随机数小于0的权重所在的机器b. 如果权重相同或者总权重数为0,就生成一个1length(权重的总个数)的随机数,此时所访问的机器就是这个随机数对应的权重所在的机器2、RoundRobin LoadBalance2.1 轮循,按公约后的权重设置轮循比率。2.2 存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。2.3 源码分析package com.alibaba.dubbo.rpc.cluster.loadbalance;import java.util.ArrayList;import java.util.List;import java.util.concurrent.ConcurrentHashMap;import java.util.concurrent.ConcurrentMap;import com.alibaba.dubbo.common.URL;import com.alibaba.dubbo.common.utils.AtomicPositiveInteger;import com.alibaba.dubbo.rpc.Invocation;import com.alibaba.dubbo.rpc.Invoker; /** Round robin load balance.** @author qian.lei* @author william.liangf*/public class RoundRobinLoadBalance extends AbstractLoadBalance {public static final String NAME = “roundrobin”; private final ConcurrentMap<String, AtomicPositiveInteger> sequences = new ConcurrentHashMap<String, AtomicPositiveInteger>();private final ConcurrentMap<String, AtomicPositiveInteger> weightSequences = new ConcurrentHashMap<String, AtomicPositiveInteger>();protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) { String key = invokers.get(0).getUrl().getServiceKey() + “.” + invocation.getMethodName(); int length = invokers.size(); // 总个数 int maxWeight = 0; // 最大权重 int minWeight = Integer.MAX_VALUE; // 最小权重 for (int i = 0; i < length; i++) { int weight = getWeight(invokers.get(i), invocation); maxWeight = Math.max(maxWeight, weight); // 累计最大权重 minWeight = Math.min(minWeight, weight); // 累计最小权重 } if (maxWeight > 0 && minWeight < maxWeight) { // 权重不一样 AtomicPositiveInteger weightSequence = weightSequences.get(key); if (weightSequence == null) { weightSequences.putIfAbsent(key, new AtomicPositiveInteger()); weightSequence = weightSequences.get(key); } int currentWeight = weightSequence.getAndIncrement() % maxWeight; List<Invoker<T>> weightInvokers = new ArrayList<Invoker<T>>(); for (Invoker<T> invoker : invokers) { // 筛选权重大于当前权重基数的Invoker if (getWeight(invoker, invocation) > currentWeight) { weightInvokers.add(invoker); } } int weightLength = weightInvokers.size(); if (weightLength == 1) { return weightInvokers.get(0); } else if (weightLength > 1) { invokers = weightInvokers; length = invokers.size(); } } AtomicPositiveInteger sequence = sequences.get(key); if (sequence == null) { sequences.putIfAbsent(key, new AtomicPositiveInteger()); sequence = sequences.get(key); } // 取模轮循 return invokers.get(sequence.getAndIncrement() % length);}}说明:从源码可以看出轮循负载均衡的算法是:a. 如果权重不一样时,获取一个当前的权重基数,然后从权重集合中筛选权重大于当前权重基数的集合,如果筛选出的集合的长度为1,此时所访问的机器就是集合里面的权重对应的机器b. 如果权重一样时就取模轮循3、LeastActive LoadBalance3.1 最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差(调用前的时刻减去响应后的时刻的值)。3.2 使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大3.3 对应的源码package com.alibaba.dubbo.rpc.cluster.loadbalance;import java.util.List;import java.util.Random;import com.alibaba.dubbo.common.Constants;import com.alibaba.dubbo.common.URL;import com.alibaba.dubbo.rpc.Invocation;import com.alibaba.dubbo.rpc.Invoker;import com.alibaba.dubbo.rpc.RpcStatus;/*** LeastActiveLoadBalance* * @author william.liangf*/public class LeastActiveLoadBalance extends AbstractLoadBalance {public static final String NAME = “leastactive”;private final Random random = new Random();protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) { int length = invokers.size(); // 总个数 int leastActive = -1; // 最小的活跃数 int leastCount = 0; // 相同最小活跃数的个数 int[] leastIndexs = new int[length]; // 相同最小活跃数的下标 int totalWeight = 0; // 总权重 int firstWeight = 0; // 第一个权重,用于于计算是否相同 boolean sameWeight = true; // 是否所有权重相同 for (int i = 0; i < length; i++) { Invoker<T> invoker = invokers.get(i); int active = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName()).getActive(); // 活跃数 int weight = invoker.getUrl().getMethodParameter(invocation.getMethodName(), Constants.WEIGHT_KEY, Constants.DEFAULT_WEIGHT); // 权重 if (leastActive == -1 || active < leastActive) { // 发现更小的活跃数,重新开始 leastActive = active; // 记录最小活跃数 leastCount = 1; // 重新统计相同最小活跃数的个数 leastIndexs[0] = i; // 重新记录最小活跃数下标 totalWeight = weight; // 重新累计总权重 firstWeight = weight; // 记录第一个权重 sameWeight = true; // 还原权重相同标识 } else if (active == leastActive) { // 累计相同最小的活跃数 leastIndexs[leastCount ++] = i; // 累计相同最小活跃数下标 totalWeight += weight; // 累计总权重 // 判断所有权重是否一样 if (sameWeight && i > 0 && weight != firstWeight) { sameWeight = false; } } } // assert(leastCount > 0) if (leastCount == 1) { // 如果只有一个最小则直接返回 return invokers.get(leastIndexs[0]); } if (! sameWeight && totalWeight > 0) { // 如果权重不相同且权重大于0则按总权重数随机 int offsetWeight = random.nextInt(totalWeight); // 并确定随机值落在哪个片断上 for (int i = 0; i < leastCount; i++) { int leastIndex = leastIndexs[i]; offsetWeight -= getWeight(invokers.get(leastIndex), invocation); if (offsetWeight <= 0) return invokers.get(leastIndex); } } // 如果权重相同或权重为0则均等随机 return invokers.get(leastIndexs[random.nextInt(leastCount)]);}}说明:源码里面的注释已经很清晰了,大致的意思就是活跃数越小的的机器分配到的请求越多4、ConsistentHash LoadBalance4.1 一致性 Hash,相同参数的请求总是发到同一提供者。4.2 当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。4.3 缺省只对第一个参数 Hash,如果要修改,请配置 <dubbo:parameter key=“hash.arguments” value=“0,1” />4.4 缺省用 160 份虚拟节点,如果要修改,请配置 <dubbo:parameter key=“hash.nodes” value=“320” />4.5 源码分析暂时还没有弄懂,后面弄懂了再补充进来,有兴趣的小伙伴可以自己去看一下源码,然后一起交流一下心得如果想免费学习Java工程化、高性能及分布式、深入浅出。微服务、Spring,MyBatis,Netty源码分析的朋友可以加我的Java进阶群:478030634,群里有阿里大牛直播讲解技术,以及Java大型互联网技术的视频免费分享给大家。5、dubbo官方的文档的负载均衡配置示例服务端服务级别 <dubbo:service interface="…" loadbalance=“roundrobin” />客户端服务级别 <dubbo:reference interface="…" loadbalance=“roundrobin” />服务端方法级别 <dubbo:service interface="…"> <dubbo:method name="…" loadbalance=“roundrobin”/> </dubbo:service>客户端方法级别 <dubbo:reference interface="…"> <dubbo:method name="…" loadbalance=“roundrobin”/> </dubbo:reference> ...

October 25, 2018 · 4 min · jiezi

记录一次亲身经历的dubbo项目实战

一、案例说明存在2个系统,A系统和B系统,A系统调用B系统的接口获取数据,用于查询用户列表。二、环境搭建安装zookeeper,解压(zookeeper-3.4.8.tar.gz)得到如下:然后进入conf将zoo_sample.cfg改名成zoo.cfg。并相关如下内容:该目录为存放数据的目录。然后启动,在bin目录下:三、工程创建1、搭建B工程1.导入依赖<dependencies><!– dubbo采用spring配置方式,所以需要导入spring容器依赖 –><dependency><groupId>org.springframework</groupId><artifactId>spring-webmvc</artifactId><version>4.1.3.RELEASE</version></dependency><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-log4j12</artifactId><version>1.6.4</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>dubbo</artifactId><version>2.5.3</version><exclusions><exclusion><!– 排除传递spring依赖 –><artifactId>spring</artifactId><groupId>org.springframework</groupId></exclusion></exclusions></dependency><!– zookeeper依赖 –><dependency><groupId>org.apache.zookeeper</groupId><artifactId>zookeeper</artifactId><version>3.3.3</version></dependency><dependency><groupId>com.github.sgroschupf</groupId><artifactId>zkclient</artifactId><version>0.1</version></dependency></dependencies>2.创建对象public class User implements Serializable{//序列化自动生成private static final long serialVersionUID = 1749666453251148943L;private Long id;private String username;private String password;private Integer age; //getter and setter}3.创建服务public class UserServiceImpl implements UserService {//实现查询,这里做模拟实现,不做具体的数据库查询public List<User> queryAll() {List<User> list = new ArrayList<User>();for (int i = 0; i < 10; i++) {User user = new User();user.setAge(10 + i);user.setId(Long.valueOf(i + 1));user.setPassword(“123456”);user.setUsername(“username_” + i);list.add(user);}return list;}}4.编写Dubbo的配置文件位置我放在根目录下dubbo/dubbo-server.xml,内容如下:<beans xmlns=“http://www.springframework.org/schema/beans"xmlns:context="http://www.springframework.org/schema/context" xmlns:p=“http://www.springframework.org/schema/p"xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx=“http://www.springframework.org/schema/tx"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dubbo=“http://code.alibabatech.com/schema/dubbo"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context-4.0.xsdhttp://www.springframework.org/schema/aophttp://www.springframework.org/schema/aop/spring-aop-4.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsdhttp://code.alibabatech.com/schema/dubbohttp://code.alibabatech.com/schema/dubbo/dubbo.xsd"><!-- 提供方应用信息,用于计算依赖关系 –><dubbo:application name=“dubbo-b-server” /><!– 这里使用的注册中心是zookeeper –><dubbo:registry address=“zookeeper://127.0.0.1:2181” client=“zkclient”/><!– 用dubbo协议在20880端口暴露服务 –><dubbo:protocol name=“dubbo” port=“20880” /><!– 将该接口暴露到dubbo中 –><dubbo:service interface=“com.shen.dubbo.service.UserService” ref=“userServiceImpl” /><!– 将具体的实现类加入到Spring容器中 –><bean id=“userServiceImpl” class=“com.shen.dubbo.service.impl.UserServiceImpl” /></beans>5.编写Web.xml<?xml version=“1.0” encoding=“UTF-8”?><web-app xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xmlns=“http://java.sun.com/xml/ns/javaee" xsi:schemaLocation=“http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id=“WebApp_ID” version=“2.5”><display-name>dubbo-b</display-name><context-param><param-name>contextConfigLocation</param-name><param-value>classpath:dubbo/dubbo-.xml</param-value></context-param><!–Spring的ApplicationContext 载入 –><listener><listener-class>org.springframework.web.context.ContextLoaderListener</listener-class></listener></web-app>6.启动tomcat在控制台中将会看到如下内容:可以看到,已经将UserService服务注册到zookeeper注册中心,协议采用的是dubbo。2、搭建A工程1.拷贝基本文件从b系统中拷贝User对象、UserService接口到a系统2.编写Dubbo的配置文件<!– 提供方应用信息,用于计算依赖关系 –><dubbo:application name=“dubbo-a-consumer” /><!– 这里使用的注册中心是zookeeper –><dubbo:registry address=“zookeeper://127.0.0.1:2181” client=“zkclient”/><!– 从注册中心中查找服务 –><dubbo:reference id=“userService” interface=“com.shen.dubbo.service.UserService”/>3.编写UserService测试用例public class UserServiceTest {private UserService userService;@Beforepublic void setUp() throws Exception {ApplicationContext applicationContext = new ClassPathXmlApplicationContext(“classpath:dubbo/.xml”);this.userService = applicationContext.getBean(UserService.class);}@Testpublic void testQueryAll() {List<User> users = this.userService.queryAll();for (User user : users) {System.out.println(user);}}}查看效果如下:可以看到,已经查询到10条数据,那么,也就是说A系统通过B系统提供的服务获取到了数据。在此我向大家推荐一个架构学习交流群。交流学习群号:478030634 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多3、解决代码重复问题我们可以看到,在上面的案例中User实体和服务接口两个项目都需要使用,代码复用不高。那么我们可以将该部分代码抽取出来打成包,以供所有系统使用。故可以在创建一个工程项目名为dubbo-b-api。然后将相关的代码都放到该项目中,再在其它项目中导入该项目依赖即可。这也是我们在真实项目中应该做的事情,因为调用方未必知道细节。 ...

October 23, 2018 · 1 min · jiezi

Spring事务事件监控

前面我们讲到了Spring在进行事务逻辑织入的时候,无论是事务开始,提交或者回滚,都会触发相应的事务事件。本文首先会使用实例进行讲解Spring事务事件是如何使用的,然后会讲解这种使用方式的实现原理。1.示例对于事务事件,Spring提供了一个注解@TransactionEventListener,将这个注解标注在某个方法上,那么就将这个方法声明为了一个事务事件处理器,而具体的事件类型则是由TransactionalEventListener.phase属性进行定义的。如下是TransactionalEventListener的声明:@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})@Retention(RetentionPolicy.RUNTIME)@Documented@EventListenerpublic @interface TransactionalEventListener {// 指定当前标注方法处理事务的类型TransactionPhase phase() default TransactionPhase.AFTER_COMMIT;// 用于指定当前方法如果没有事务,是否执行相应的事务事件监听器boolean fallbackExecution() default false;// 与classes属性一样,指定了当前事件传入的参数类型,指定了这个参数之后就可以在监听方法上// 直接什么一个这个参数了@AliasFor(annotation = EventListener.class, attribute = “classes”)Class<?>[] value() default {};// 作用于value属性一样,用于指定当前监听方法的参数类型@AliasFor(annotation = EventListener.class, attribute = “classes”)Class<?>[] classes() default {};// 这个属性使用Spring Expression Language对目标类和方法进行匹配,对于不匹配的方法将会过滤掉String condition() default “”;}关于这里的classes属性需要说明一下,如果指定了classes属性,那么当前监听方法的参数类型就可以直接使用所发布的事件的参数类型,如果没有指定,那么这里监听的参数类型可以使用两种:ApplicationEvent和PayloadApplicationEvent。对于ApplicationEvent类型的参数,可以通过其getSource()方法获取发布的事件参数,只不过其返回值是一个Object类型的,如果想获取具体的类型还需要进行强转;对于PayloadApplicationEvent类型,其可以指定一个泛型参数,该泛型参数必须与发布的事件的参数类型一致,这样就可以通过其getPayload()方法获取事务事件发布的数据了。关于上述属性中的TransactionPhase,其可以取如下几个类型的值:public enum TransactionPhase { // 指定目标方法在事务commit之前执行 BEFORE_COMMIT, // 指定目标方法在事务commit之后执行 AFTER_COMMIT, // 指定目标方法在事务rollback之后执行 AFTER_ROLLBACK, // 指定目标方法在事务完成时执行,这里的完成是指无论事务是成功提交还是事务回滚了 AFTER_COMPLETION } 这里我们假设数据库有一个user表,对应的有一个UserService和User的model,用于往该表中插入数据,并且插入动作时使用注解标注目标方法。如下是这几个类的声明:public class User {private long id;private String name;private int age;// getter and setter…}.@Service@Transactionalpublic class UserServiceImpl implements UserService {@Autowiredprivate JdbcTemplate jdbcTemplate; @Autowired private ApplicationEventPublisher publisher;@Overridepublic void insert(User user) {jdbcTemplate.update(“insert into user (id, name, age) value (?, ?, ?)”, user.getId(), user.getName(), user.getAge());publisher.publishEvent(user);}}上述代码中有一点需要注意的是,对于需要监控事务事件的方法,在目标方法执行的时候需要使用ApplicationEventPublisher发布相应的事件消息。如下是对上述消息进行监控的程序:@Componentpublic class UserTransactionEventListener {@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)public void beforeCommit(PayloadApplicationEvent<User> event) {System.out.println(“before commit, id: " + event.getPayload().getId());}@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)public void afterCommit(PayloadApplicationEvent<User> event) {System.out.println(“after commit, id: " + event.getPayload().getId());}@TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION)public void afterCompletion(PayloadApplicationEvent<User> event) {System.out.println(“after completion, id: " + event.getPayload().getId());}@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)public void afterRollback(PayloadApplicationEvent<User> event) {System.out.println(“after rollback, id: " + event.getPayload().getId());}}这里对于事件的监控,只需要在监听方法上添加@TransactionalEventListener注解即可。这里需要注意的一个问题,在实际使用过程中,对于监听的事务事件,需要使用其他的参数进行事件的过滤,因为这里的监听还是会监听所有事件参数为User类型的事务,而无论其是哪个位置发出来的。如果需要对事件进行过滤,这里可以封装一个UserEvent对象,其内保存一个类似EventType的属性和一个User对象,这样在发布消息的时候就可以指定EventType属性,而在监听消息的时候判断当前方法监听的事件对象的EventType是否为目标type,如果是,则对其进行处理,否则直接略过。下面是上述程序的xml文件配置和驱动程序:<bean id=“dataSource” class=“org.apache.commons.dbcp.BasicDataSource”><property name=“url” value=“jdbc:mysql://localhost/test?useUnicode=true”/><property name=“driverClassName” value=“com.mysql.jdbc.Driver”/><property name=“username” value=””/><property name=“password” value=””/></bean><bean id=“jdbcTemplate” class=“org.springframework.jdbc.core.JdbcTemplate”><property name=“dataSource” ref=“dataSource”/></bean><bean id=“transactionManager” class=“org.springframework.jdbc.datasource.DataSourceTransactionManager”><property name=“dataSource” ref=“dataSource”/></bean><context:component-scan base-package=“com.transaction”/><tx:annotation-driven/>.public class TransactionApp {@Testpublic void testTransaction() {ApplicationContext ac = new ClassPathXmlApplicationContext(“applicationContext.xml”);UserService userService = context.getBean(UserService.class);User user = getUser();userService.insert(user);}private User getUser() {int id = new Random() .nextInt(1000000);User user = new User();user.setId(id);user.setName(“Mary”);user.setAge(27);return user;}}运行上述程序,其执行结果如下:before commit, id: 935052after commit, id: 935052after completion, id: 935052可以看到,这里确实成功监听了目标程序的相关事务行为。2.实现原理关于事务的实现原理,这里其实是比较简单的,在前面的文章中,我们讲解到,Spring对事务监控的处理逻辑在TransactionSynchronization中,如下是该接口的声明:public interface TransactionSynchronization extends Flushable {// 在当前事务挂起时执行default void suspend() {}// 在当前事务重新加载时执行default void resume() {}// 在当前数据刷新到数据库时执行default void flush() {}// 在当前事务commit之前执行default void beforeCommit(boolean readOnly) {}// 在当前事务completion之前执行default void beforeCompletion() {}// 在当前事务commit之后实质性default void afterCommit() {}// 在当前事务completion之后执行default void afterCompletion(int status) {}}很明显,这里的TransactionSynchronization接口只是抽象了一些行为,用于事务事件发生时触发,这些行为在Spring事务中提供了内在支持,即在相应的事务事件时,其会获取当前所有注册的TransactionSynchronization对象,然后调用其相应的方法。那么这里TransactionSynchronization对象的注册点对于我们了解事务事件触发有至关重要的作用了。这里我们首先回到事务标签的解析处,在前面讲解事务标签解析时,我们讲到Spring会注册一个TransactionalEventListenerFactory类型的bean到Spring容器中,这里关于标签的解析读者可以阅读本人前面的文章Spring事务用法示例与实现原理。这里注册的TransactionalEventListenerFactory实现了EventListenerFactory接口,这个接口的主要作用是先判断目标方法是否是某个监听器的类型,然后为目标方法生成一个监听器,其会在某个bean初始化之后由Spring调用其方法用于生成监听器。如下是该类的实现:public class TransactionalEventListenerFactory implements EventListenerFactory, Ordered {// 指定当前监听器的顺序private int order = 50;public void setOrder(int order) { this.order = order;}@Overridepublic int getOrder() { return this.order;}// 指定目标方法是否是所支持的监听器的类型,这里的判断逻辑就是如果目标方法上包含有// TransactionalEventListener注解,则说明其是一个事务事件监听器@Overridepublic boolean supportsMethod(Method method) { return (AnnotationUtils.findAnnotation(method, TransactionalEventListener.class) != null);}// 为目标方法生成一个事务事件监听器,这里ApplicationListenerMethodTransactionalAdapter实现了// ApplicationEvent接口@Overridepublic ApplicationListener<?> createApplicationListener(String beanName, Class<?> type, Method method) { return new ApplicationListenerMethodTransactionalAdapter(beanName, type, method);}}这里关于事务事件监听的逻辑其实已经比较清楚了。ApplicationListenerMethodTransactionalAdapter本质上是实现了ApplicationListener接口的,也就是说,其是Spring的一个事件监听器,这也就是为什么进行事务处理时需要使用ApplicationEventPublisher.publish()方法发布一下当前事务的事件。ApplicationListenerMethodTransactionalAdapter在监听到发布的事件之后会生成一个TransactionSynchronization对象,并且将该对象注册到当前事务逻辑中,如下是监听事务事件的处理逻辑:@Override public void onApplicationEvent(ApplicationEvent event) {// 如果当前TransactionManager已经配置开启事务事件监听,// 此时才会注册TransactionSynchronization对象if (TransactionSynchronizationManager.isSynchronizationActive()) { // 通过当前事务事件发布的参数,创建一个TransactionSynchronization对象 TransactionSynchronization transactionSynchronization = createTransactionSynchronization(event); // 注册TransactionSynchronization对象到TransactionManager中 TransactionSynchronizationManager .registerSynchronization(transactionSynchronization);} else if (this.annotation.fallbackExecution()) { // 如果当前TransactionManager没有开启事务事件处理,但是当前事务监听方法中配置了 // fallbackExecution属性为true,说明其需要对当前事务事件进行监听,无论其是否有事务 if (this.annotation.phase() == TransactionPhase.AFTER_ROLLBACK && logger.isWarnEnabled()) { logger.warn(“Processing " + event + " as a fallback execution on AFTER_ROLLBACK phase”); } processEvent(event);} else { // 走到这里说明当前是不需要事务事件处理的,因而直接略过 if (logger.isDebugEnabled()) { logger.debug(“No transaction is active - skipping " + event); }}}这里需要说明的是,上述annotation属性就是在事务监听方法上解析的TransactionalEventListener注解中配置的属性。可以看到,对于事务事件的处理,这里创建了一个TransactionSynchronization对象,其实主要的处理逻辑就是在返回的这个对象中,而createTransactionSynchronization()方法内部只是创建了一个TransactionSynchronizationEventAdapter对象就返回了。这里我们直接看该对象的源码: private static class TransactionSynchronizationEventAdapter extends TransactionSynchronizationAdapter { private final ApplicationListenerMethodAdapter listener; private final ApplicationEvent event; private final TransactionPhase phase;public TransactionSynchronizationEventAdapter(ApplicationListenerMethodAdapter listener, ApplicationEvent event, TransactionPhase phase) { this.listener = listener; this.event = event; this.phase = phase;}@Overridepublic int getOrder() { return this.listener.getOrder();}// 在目标方法配置的phase属性为BEFORE_COMMIT时,处理before commit事件public void beforeCommit(boolean readOnly) { if (this.phase == TransactionPhase.BEFORE_COMMIT) { processEvent(); }}// 这里对于after completion事件的处理,虽然分为了三个if分支,但是实际上都是执行的processEvent()// 方法,因为after completion事件是事务事件中一定会执行的,因而这里对于commit,// rollback和completion事件都在当前方法中处理也是没问题的public void afterCompletion(int status) { if (this.phase == TransactionPhase.AFTER_COMMIT && status == STATUS_COMMITTED) { processEvent(); } else if (this.phase == TransactionPhase.AFTER_ROLLBACK && status == STATUS_ROLLED_BACK) { processEvent(); } else if (this.phase == TransactionPhase.AFTER_COMPLETION) { processEvent(); }}// 执行事务事件protected void processEvent() { this.listener.processEvent(this.event);}}可以看到,对于事务事件的处理,最终都是委托给了ApplicationListenerMethodAdapter.processEvent()方法进行的。如下是该方法的源码: public void processEvent(ApplicationEvent event) {// 处理事务事件的相关参数,这里主要是判断TransactionalEventListener注解中是否配置了value// 或classes属性,如果配置了,则将方法参数转换为该指定类型传给监听的方法;如果没有配置,则判断// 目标方法是ApplicationEvent类型还是PayloadApplicationEvent类型,是则转换为该类型传入Object[] args = resolveArguments(event);// 这里主要是获取TransactionalEventListener注解中的condition属性,然后通过// Spring expression language将其与目标类和方法进行匹配if (shouldHandle(event, args)) { // 通过处理得到的参数借助于反射调用事务监听方法 Object result = doInvoke(args); if (result != null) { // 对方法的返回值进行处理 handleResult(result); } else { logger.trace(“No result object given - no result to handle”); }} } // 处理事务监听方法的参数protected Object[] resolveArguments(ApplicationEvent event) {// 获取发布事务事件时传入的参数类型ResolvableType declaredEventType = getResolvableType(event);if (declaredEventType == null) { return null;}// 如果事务监听方法的参数个数为0,则直接返回if (this.method.getParameterCount() == 0) { return new Object[0];}// 如果事务监听方法的参数不为ApplicationEvent或PayloadApplicationEvent,则直接将发布事务// 事件时传入的参数当做事务监听方法的参数传入。从这里可以看出,如果事务监听方法的参数不是// ApplicationEvent或PayloadApplicationEvent类型,那么其参数必须只能有一个,并且这个// 参数必须与发布事务事件时传入的参数一致Class<?> eventClass = declaredEventType.getRawClass();if ((eventClass == null || !ApplicationEvent.class.isAssignableFrom(eventClass)) && event instanceof PayloadApplicationEvent) { return new Object[] {((PayloadApplicationEvent) event).getPayload()};} else { // 如果参数类型为ApplicationEvent或PayloadApplicationEvent,则直接将其传入事务事件方法 return new Object[] {event};} } // 判断事务事件方法方法是否需要进行事务事件处理private boolean shouldHandle(ApplicationEvent event, @Nullable Object[] args) {if (args == null) { return false;}String condition = getCondition();if (StringUtils.hasText(condition)) { Assert.notNull(this.evaluator, “EventExpressionEvaluator must no be null”); EvaluationContext evaluationContext = this.evaluator.createEvaluationContext( event, this.targetClass, this.method, args, this.applicationContext); return this.evaluator.condition(condition, this.methodKey, evaluationContext);}return true; } // 对事务事件方法的返回值进行处理,这里的处理方式主要是将其作为一个事件继续发布出去,这样就可以在// 一个统一的位置对事务事件的返回值进行处理protected void handleResult(Object result) {// 如果返回值是数组类型,则对数组元素一个一个进行发布if (result.getClass().isArray()) { Object[] events = ObjectUtils.toObjectArray(result); for (Object event : events) { publishEvent(event); }} else if (result instanceof Collection<?>) { // 如果返回值是集合类型,则对集合进行遍历,并且发布集合中的每个元素 Collection<?> events = (Collection<?>) result; for (Object event : events) { publishEvent(event); }} else { // 如果返回值是一个对象,则直接将其进行发布 publishEvent(result);}}对于事务事件的处理,总结而言,就是为每个事务事件监听方法创建了一个TransactionSynchronizationEventAdapter对象,通过该对象在发布事务事件的时候,会在当前线程中注册该对象,这样就可以保证每个线程每个监听器中只会对应一个TransactionSynchronizationEventAdapter对象。在Spring进行事务事件的时候会调用该对象对应的监听方法,从而达到对事务事件进行监听的目的。在此我向大家推荐一个架构学习交流群。交流学习群号:478030634 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多3.小结本文首先对事务事件监听程序的使用方式进行了讲解,然后在源码层面讲解了Spring事务监听器是如何实现的。在Spring事务监听器使用过程中,需要注意的是要对当前接收到的事件类型进行判断,因为不同的事务可能会发布同样的消息对象过来。 ...

October 22, 2018 · 3 min · jiezi

微服务架构组件分析

微服务架构组件1、 如何发布和引用服务服务描述:服务调用首先解决的问题就是服务如何对外描述。 常用的服务描述方式包括 RESTful API、XML 配置以及 IDL 文件三种。RESTful API主要被用作 HTTP 或者 HTTPS 协议的接口定义,即使在非微服务架构体系下,也被广泛采用优势:HTTP 协议本身是一个公开的协议,对于服务消费者来说几乎没有学习成本,所以比较适合用作跨业务平台之间的服务协议。劣势:-性能相对比较低XML 配置一般是私有 RPC 框架会选择 XML 配置这种方式来描述接口,因为私有 RPC 协议的性能比 HTTP 协议高,所以在对性能要求比较高的场景下,采用 XML 配置比较合适。这种方式的服务发布和引用主要分三个步骤:服务提供者定义接口,并实现接口服务提供者进程启动时,通过加载 server.xml 配置文件将接口暴露出去。服务消费者进程启动时,通过加载 client.xml 配置文件引入要调用的接口。优势:私有 RPC 协议的性能比 HTTP 协议高,所以在对性能要求比较高的场景下,采用 XML 配置方式比较合适 劣势:对业务代码侵入性比较高XML 配置有变更的时候,服务消费者和服务提供者都要更新(建议:公司内部联系比较紧密的业务之间采用)IDL 文件IDL 就是接口描述语言(interface description language)的缩写,通过一种中立的方式来描接口,使得在不同的平台上运行的对象和不同语言编写的程序可以相互通信交流。常用的 IDL:一个是 Facebook 开源的 Thrift 协议,另一个是 Google 开源的 gRPC 协议。无论是 Thrift 协议还是 gRPC 协议,他们的工作原来都是类似的。优势:用作跨语言平台的服务之间的调用劣势:在描述接口定义时,IDL 文件需要对接口返回值进行详细定义。如果接口返回值的字段比较多,并且经常变化时,采用 IDL文件方式的接口定义就不太合适了。一方面会造成 IDL 文件过大难以维护另一方面只要 IDL 文件中定义的接口返回值有变更,都需要同步所有的服务消费者都更新,管理成本太高了。总结具体采用哪种服务描述方式是根据实际情况决定,通常情况下, 如果只是企业内部之间的服务调用,并且都是 Java 语言的话,选择 XML 配置方式是最简单的。如果企业内部存在多个服务,并且服务采用的是不同语言平台,建议使用 IDL 文件方式进行描述服务。如果还存在对外开放服务调用的情形的话,使用 RESTful API 方式则更加通用。2、 如何注册和发现服务注册中心原理在微服务架构下, 主要有三种角色:服务提供者(RPC Server)、服务消费者(RPC Client)和服务注册中心(Registry),三者的交互关系如图RPC Server 提供服务,在启动时,根据服务发布文件 server.xml 中配置的信息,向 Registry 注册服务,把Registry 返回的服务节点列表缓存在本地内存中,并于 RPC Server 建立连接。RPC Client 调用服务,在启动时,根据服务引用文件 client.xml 中配置的信息,向 Registry 订阅服务,把Registry 返回的服务节点列表缓存在本地内存中,并于 RPC Client 建立连接。当 RPC Server 节点发生变更时,Registry 会同步变更,RPC Client 感知后会刷新本地内存中缓存的服务节点列表。RPC Client 从本地缓存的服务节点列表中,基于负载均衡算法选择一台 RPC Server 发起调用。注册中心实现方式注册中心API服务注册接口:服务提供者通过调用注册接口来完成服务注册服务反注册接口:服务提供者通过调用服务反注册接口来完成服务注销心跳汇报接口:服务提供者通过调用心跳汇报接口完成节点存货状态上报服务订阅接口:服务消费者调用服务订阅接口完成服务订阅,获取可用的服务提供者节点列表服务变更查询接口:服务消费者通过调用服务变更查询接口,获取最新的可用服务节点列表服务查询接口:查询注册中心当前住了哪些服务信息服务修改接口:修改注册中心某一服务的信息集群部署注册中心一般都是采用集群部署来保证高可用性,并通过分布式一致性协议来确保集群中不同节点之间的数据保持一致。Zookeeper 的工作原理:每个 Server 在内存中存储了一份数据,Client 的读请求可以请求任意一个 ServerZookeeper 启动时,将从实例中选举一个 leader(Paxos 协议)Leader 负责处理数据更新等操作(ZAB 协议)一个更新操作方式,Zookeeper 保证了高可用性以及数据一致性目录存储ZooKeeper作为注册中心存储服务信息一般采用层次化的目录结构:每个目录在 ZooKeeper 中叫作 znode,并且其有一个唯一的路径标识znode 可以包含数据和子 znode。znode 中的数据可以有多个版本,比如某一个 znode 下存有多个数据版本,那么查询这个路径下的数据需带上版本信息。服务健康状态检测注册中心除了要支持最基本的服务注册和服务订阅功能以外,还必须具备对服务提供者节点的健康状态检测功能,这样才能保证注册中心里保存的服务节点都是可用的。基于 ZooKeeper 客户端和服务端的长连接和会话超时控制机制,来实现服务健康状态检测的。在 ZooKeeper 中,客户端和服务端建立连接后,会话也也随之建立,并生成一个全局唯一的 SessionID。服务端和客户端维持的是一个长连接,在 SESSION_TIMEOUT周期内,服务端会检测与客户端的链路是否正常,具体方式是通过客户端定时向服务端发送心跳消息(ping 消息),服务器重置下次 SESSION_TIMEOUT 时间。如果超过 SESSION_TIMEOUT,ZooKeeper 就会认为这个 Session 就已经结束了,ZooKeeper 就会认为这个服务节点已经不可用,将会从注册中心中删除其信息。服务状态变更通知一旦注册中心探测到有服务器提供者节点新加入或者被剔除,就必须立刻通知所有订阅该服务的服务消费者,刷新本地缓存的服务节点信息,确保服务调用不会请求不可用的服务提供者节点。基于 Zookeeper 的 Watcher 机制,来实现服务状态变更通知给服务消费者的。服务消费者在调用 Zookeeper 的getData 方式订阅服务时,还可以通过监听器 Watcher 的 process 方法获取服务的变更,然后调用 getData方法来获取变更后的数据,刷新本地混存的服务节点信息。白名单机制注册中心可以提供一个白名单机制,只有添加到注册中心白名单内的 RPC Server,才能够调用注册中心的注册接口,这样的话可以避免测试环境中的节点意外跑到线上环境中去。总结注册中心可以说是实现服务话的关键,因为服务话之后,服务提供者和服务消费者不在同一个进程中运行,实现了解耦,这就需要一个纽带去连接服务提供者和服务消费者,而注册中心就正好承担了这一角色。此外,服务提供者可以任意伸缩即增加节点或者减少节点,通过服务健康状态检测,注册中心可以保持最新的服务节点信息,并将变化通知给订阅服务的服务消费者。注册中心一般采用分布式集群部署,来保证高可用性,并且为了实现异地多活,有的注册中心还采用多 IDC 部署,这就对数据一致性产生了很高的要求,这些都是注册中心在实现时必须要解决的问题。在此我向大家推荐一个架构学习交流群。交流学习群号:478030634 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多3、如何实现 RPC 远程服务调用客户端和服务端如何建立网络连接HTTP 通信HTTP 通信是基于应用层HTTP 协议的,而 HTTP 协议又是基于传输层 TCP 协议的。一次 HTTP 通信过程就是发起一次 HTTP 调用,而一次 HTTP 调用就会建立一个 TCP 连接,经历一次下图所示的 “三次握手”的过程来建立连接。完成请求后,再经历一次“四次挥手”的过程来断开连接。Socket 通信Socket 通信是基于 TCP/IP 协议的封装,建立一次Socket 连接至少需要一对套接字,其中一个运行于客户端,称为 ClientSocket ;另一个运行于服务器端,称为 ServerSocket 。服务器监听:ServerSocket 通过点用 bind() 函数绑定某个具体端口,然后调用 listen() 函数实时监控网络状态,等待客户端的连接请求。客户端请求:ClientSocket 调用 connect() 函数向 ServerSocket 绑定的地址和端口发起连接请求。服务端连接确认:当 ServerSocket 监听都或者接收到 ClientSocket 的连接请求时,调用 accept() 函数响应ClientSocket 的请求,同客户端建立连接。数据传输:当 ClientSocket 和 ServerSocket 建立连接后,ClientSocket 调用 send() 函数,ServerSocket 调用 receive() 函数,ServerSocket 处理完请求后,调用 send() 函数,ClientSocket 调用 receive() 函数,就可以得到返回结果。当客户端和服务端建立网络连接后,就可以起发起请求了。但网络不一定总是可靠的,经常会遇到网络闪断、连接超时、服务端宕机等各种异常,通常的处理手段有两种:链路存活检测:客户端需要定时地发送心跳检测小心(一般通过 ping 请求) 给服务端,如果服务端连续 n次心跳检测或者超过规定的时间没有回复消息,则认为此时链路已经失效,这个时候客户端就需要重新与服务端建立连接。断连重试:通常有多种情况会导致连接断开,比如客户端主动关闭、服务端宕机或者网络故障等。这个时候客户端就需要与服务端重新建立连接,但一般不能立刻完成重连,而是要等待固定的间隔后再发起重连,避免服务端的连接回收不及时,而客户端瞬间重连的请求太多而把服务端的连接数占满。服务端如何处理请求同步阻塞方式(BIO)客户端每发一次请求,服务端就生成一个线程去处理。当客户端同时发起的请求很多事,服务端需要创建很多的线程去处理每一个请求,如果达到了系统最大的线程数瓶颈,新来的请求就没法处理了。BIO 适用于连接数比较小的业务场景,这样的话不至于系统中没有可用线程去处理请求。这种方式写的程序也比较简单直观,易于理解。同步非阻塞(NIO)客户端每发一次请求,服务端并不是每次都创建一个新线程来处理,而是通过 I/O 多路复用技术进行处理。就是把多个 I/O 的阻塞复用到听一个 select 的阻塞上,从而使系统在单线程的情况下可以同时处理多个客户端请求。这种方式的优势是开销小,不用为每个请求创建一个线程,可以节省系统开销。NIO 适用于连接数比较多并且请求消耗比较轻的业务场景,比如聊天服务器。这种方式相比 BIO,相对来说编程比较复杂。异步非阻塞(AIO)客户端只需要发起一个 I/O 操作然后立即返回,等 I/O 操作真正完成以后,客户端会得到 I/O 操作完成的通知,此时客户端只需要对数据进行处理就好了,不需要进行实际的 I/O 读写操作,因为真正的 I/O 读取或者写入操作已经由内核完成了。这种方式的优势是客户端无需等待,不存在阻塞等待问题。AIO 适用于连接数比较多而且请求消耗比较重的业务场景,比如涉及 I/O 操作的相册服务器。这种方式相比另外两种,编程难难度最大,程序也不易于理解。建议最为稳妥的方式是使用成熟的开源方案,比如 Netty、MINA 等,它们都是经过业界大规模应用后,被充分论证是很可靠的方案。数据传输采用什么协议无论是开放的还是私有的协议,都必须定义一个“契约”,以便服务消费和服务提供者之间能够达成共识。服务消费者按照契约,对传输的数据进行编码,然后通过网络传输过去;服务提供者从网络上接收到数据后,按照契约,对传输的数据进行解码,然后处理请求,再把处理后的结果进行编码,通过网络传输返回给服务消费者;服务消费者再对返回的结果进行解码,最终得到服务提供者处理后的返回值。HTTP 协议消息头Server 代表是服务端服务器类型Content-Length 代表返回数据的长度Content-Type 代表返回数据的类型消息体具体的返回结果数据该如何序列化和反序列化一般数据在网络中进行传输,都要先在发送方一段对数据进行编码,经过网络传输到达另一段后,再对数据进行解码,这个过程就是序列化和反序列化常用的序列化方式分为两类:文本类如 XML/JSON 等,二进制类如 PB/Thrift 等,而具体采用哪种序列化方式,主要取决于三个方面的因素。支持数据结构类型的丰富度。数据结构种类支持的越多越好,这样的话对于使用者来说在编程时更加友好,有些序列化框架如 Hessian 2.0还支持复杂的数据结构比如 Map、List等。跨语言支持。性能。主要看两点,一个是序列化后的压缩比,一个是序列化的速度。以常用的 PB 序列化和 JSON 序列化协议为例来对比分析,PB序列化的压缩比和速度都要比 JSON 序列化高很多,所以对性能和存储空间要求比较高的系统选用 PB 序列化更合;而 JSON序列化虽然性能要差一些,但可读性更好,所以对性能和存储空间要求比较高的系统选用 PB 序列化更合适对外部提供服务。总结通信框架:它主要解决客户端和服务端如何建立连接、管理连接以及服务端如何处理请求的问题。通信协议:它主要解决客户端和服务端采用哪些数据传输协议的问题。序列化和反序列化:它主要解决客户端和服务端采用哪种数据编码的问题。这三部分就组成了一个完成的RPC 调用框架,通信框架提供了基础的通信能力,通信协议描述了通信契约,而序列化和反序列化则用于数据的编/解码。一个通信框架可以适配多种通信协议,也可以采用多种序列化和反序列化的格式,比如服务话框架 不仅支持 Dubbo 协议,还支持 RMI 协议、HTTP 协议等,而且还支持多种序列化和反序列化格式,比如 JSON、Hession 2.0 以及 Java 序列化等。4、如何监控微服务调用在谈论监控微服务监控调用前,首先要搞清楚三个问题:监控的对象是什么?具体监控哪些指标?从哪些维度进行监控?监控对象用户端监控:通常是指业务直接对用户提供的功能的监控。接口监控:通常是指业务提供的功能所以来的具体 RPC 接口监控。资源监控:通常是指某个接口依赖的资源的监控。(eg:Redis 来存储关注列表,对 Redis 的监控就属于资源监控。)基础监控:通常是指对服务器本身的健康状况的监控。(eg: CPU、MEM、I/O、网卡带宽等)监控指标1、请求量实时请求量(QPS Queries Per Second):即每秒查询次数来衡量,反映了服务调用的实时变化情况统计请求量(PV Page View):即一段时间内用户的访问量来衡量,eg:一天的 PV 代表了服务一天的请求量,通常用来统计报表2、响应时间:大多数情况下,可以用一段时间内所有调用的平均耗时来反应请求的响应时间。但它只代表了请求的平均快慢情况,有时候我们更关心慢请求的数量。为此需要把响应时间划分为多个区间,比如0~10ms、10ms~50ms、50ms~100ms、100ms~500ms、500ms 以上这五个区间,其中 500ms 以上这个区间内的请求数就代表了慢请求量,正常情况下,这个区间内的请求数应该接近于 0;在出现问题时,这个区间内的请求数应该接近于 0;在出现问题时,这个区间内的请求数会大幅增加,可能平均耗时并不能反映出这一变化。除此之外,还可以从P90、P95、P99、P999 角度来监控请求的响应时间在 500ms 以内,它代表了请求的服务质量,即 SLA。3、错误率:通常用一段时间内调用失败的次数占调用总次数的比率来衡量,比如对于接口的错误率一般用接口返回错误码为 503 的比率来表示。监控维度全局维度:从整体角度监控对象的请求量、平均耗时以及错误率,全局维度的监控一般是为了让你对监控对象的调用情况有个整体了解。分机房维度:为了业务高可用,服务部署不止一个机房,因为不同机房地域的不同,同一个监控对象的各种指标可能会相差很大。单机维度:同一个机房内部,可能由于采购年份和批次不的不同,各种指标也不一样。时间维度:同一个监控对象,在每天的同一时刻各种指标通常也不会一样,这种差异要么是由业务变更导致,要么是运营活动导致。为了了解监控对象各种指标的变化,通常需要与一天前、一周前、一个月前,甚至三个月前比较。核心维度:业务上一般会依据重要性成都对监控对象进行分级,最简单的是分成核心业务和非核心业务。核心业务和非核心业务在部署上必须隔离,分开监控,这样才能对核心业务做重点保障。对于一个微服务来说,必须要明确监控哪些对象、哪些指标,并且还要从不同的维度进行监控,才能掌握微服务的调用情况。监控系统原理数据采集:收集到每一次调用的详细信息,包括调用的响应时间、调用是否成功、调用的发起者和接收者分别是谁,这个过程叫做数据采集。数据传输:采集到数据之后,要把数据通过一定的方式传输给数据处理中心进行处理,这个过程叫做数据出传输。数据处理:数据传输过来后,数据处理中心再按照服务的维度进行聚合,计算出不同服务的请求量、响应时间以及错误率等信息并存储起来,这个过程叫做数据处理。数据展示:通过接口或者 DashBoard 的形式对外展示服务的调用情况,这个过程叫做数据展示。数据采集服务主动上报代理收集:这种处理方式通过服务调用后把调用的详细信息记录到本地日志文件中,然后再通过代理去解析本地日志文件,然后再上报服务的调用信息。不管是哪种方式,首先要考虑的问题就是采样率,也就是采集数据的频率。一般来说,采样率越高,监控的实时性就越高,精确度也越高。但采样对系统本身的性能也会有一定的影响,尤其是采集后的数据需要写到本地磁盘的时候,过高的采样率会导致系统写入的 I/O 过高,进而会影响到正常的服务调用。所以合理的采样率是数据采集的关键,最好是可以动态控制采样率,在系统比较空闲的时候加大采样率,追求监控的实时性与精确度;在系统负载比较高的时候减少采样率,追求监控的可用性与系统的稳定性。数据传输UDP传输:这种处理方式是数据处理单元提供服务器的请求地址,数据采集后通过 UDP 协议与服务器建立连接,然后把数据发送过去。Kafka传输:这种处理方式是数据采集后发送都指定的 Topic,然后数据处理单元再订阅对应的 Topic,就可以从 Kafka 消息队列中读取对应的数据。无论哪种传输方式,数据格式十分重要,尤其是对带宽敏感以及解析性能要求比较高的场景,一般数据传输时采用的数据格式有两种:二进制协议,最常用的就是 PB 对象文本协议,最常用的就是 JSON 字符串数据处理接口维度聚合:把实时收到的数据按照调用的节点维度聚合在一起,这样就可以得到每个接口的实时请求、平均耗时等信息。机器维度聚合:把实时收到的数据按照调用的节点维度聚合在一起,这样就可以从单机维度去查看每个接口的实时请求量、平均耗时等信息。聚合后的数据需要持久化到数据库中存储,所选用的数据库一般分为两种:索引数据库:比如 Elasticsearcher,以倒排索引的数据结构存书,需要查询的时候,根据索引来查询。时序数据库:比如 OpenTSDB,以时序序列数据的方式存储,查询的时候按照时序如 1min、5min 等维度查询数据展示曲线图:监控变化趋势。饼状图:监控占比分布。格子图:主要坐一些细粒度的监控。总结服务监控子啊微服务改造过程中的重要性不言而喻,没有强大的监控能力,改造成微服务架构后,就无法掌控各个不同服务的情况,在遇到调用失败时,如果不能快速发现系统的问题,对于业务来说就是一场灾难。搭建一个服务监控系统,设计数据采集、数据传输、数据处理、数据展示等多个环节,每个环节都需要根据自己的业务特点选择合适的解决方案在此我向大家推荐一个架构学习交流群。交流学习群号:478030634 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多5、如何追踪微服务调用跟踪记录一次用户请求都发起了哪些调用,经过哪些服务处理,并且记录每一次调用所涉及的详细信息,这时候如果发生调用失败,就可以通过这个日志快速定位是在哪个环节出了问题。服务追踪的作用优化系统瓶颈通过记录调用经过的每一条链路上的耗时,可以快速定位整个系统的瓶颈点在哪里。可能出现的原因如下:运营商网络延迟网关系统异常某个服务异常缓存或者数据库异常通过服务追踪,可以从全局视角上去观察,找出整个系统的瓶颈点所在,然后做出针对性的优化优化链路调用通过服务追踪可以分析调用所经过的路径,然后评估是否合理一般业务都会在多个数据中心都部署服务,以实现异地容灾,这个时候经常会出现一种状况就是服务 A 调用了另外一个数据中心的服务B,而没有调用同处于一个数据中心的服务B。跨数据中心的调用视距离远近都会有一定的网络延迟,像北京和广州这种几千公里距离的网络延迟可能达到了30ms以上,这对于有些业务几乎是不可接受的。通过对调用链路进行分析,可以找出跨数据中的服务调用,从而进行优化,尽量规避这总情况出现。生成网络拓扑通过服务追踪系统中记录的链路信息,可以生成一张系统的网络调用拓扑图,它可以反映系统都依赖了哪些服务,以及服务之间的调用关系是什么样的,可以一目了然。除此之外,在网络拓扑图上还可以把服务调用的详细信息也标出来,也能起到服务监控的作用。透明传输数据除了服务追踪,业务上经常有一种需求,期望能把一些用户数据,从调用的开始一直往下传递,以便系统中的各个服务都能获取到这个信息。比如业务想做一些A/B 测试,这时候就想通过服务追踪系统,把 A/B 测试的开关逻辑一直往下传递,经过的每一层服务都能获取到这个开关值,就能够统一进行A/B 测试。服务追踪原理服务追踪鼻祖:Google 发布的一篇的论文Dapper, [a Large-Scale Distributed Systems Tracing Infrastructure核心理念:通过一个全局唯一的 ID将分布在各个服务节点上的同一次请求串联起来,从而还原原有的调用关系,可以追踪系统问题、分析调用数据并统计各种系统指标可以说后面的诞生各种服务追踪系统都是基于 Dapper 衍生出来的,比较有名的有 Twitter的Zipkin、阿里的鹰眼、美团的MTrace等。讲解下服务追踪系统中几个最基本概念traceId:用于标识某一次具体的请求ID。spanId:用于标识一次 RPC 调用在分布式请求中的位置。annotation:用于业务自定义埋点数据,可以是业务感兴趣的上上传到后端的数据,比如一次请求的用户 UID。traceId 是用于串联某一次请求在系统中经过的所有路径,spanId 是用于区分系统不同服务之间调用的先后关系,而annotation 是用于业务自定义一些自己感兴趣的数据,在上传 traceId 和 spanId 这些基本信息之外,添加一些自己感兴趣的信息。服务追踪系统实现上面是服务追踪系统架构图,一个服务追踪系统可以分三层:数据采集层:负责数据埋点并上报数据处理层:负责数据的存储与计算数据展示层:负责数据的图形化展示数据采集层作用:在系统的各个不同的模块中尽心埋点,采集数据并上报给数据处理层进行处理。CS(Client Send)阶段 : 客户端发起请求,并生成调用的上下文。SR(Server Recieve)阶段 : 服务端接收请求,并生成上下文。SS(Server Send)阶段 :服务端返回请求,这个阶段会将服务端上下文数据上报,下面这张图可以说明上报的数据有:traceId=123456,spanId=0.1,appKey=B,method=B.method,start=103,duration=38.CR(Client Recieve)阶段 :客户端接收返回结果,这个阶段会将客户端上下文数据上报,上报的数据有:traceid=123456,spanId=0.1,appKey=A,method=B.method,start=103,duration=38。数据处理层作用:把数据上报的数据按需计算,然后落地存储供查询使用实时数据处理:要求计算效率比较高,一般要对收集的链路数据能够在秒级别完成聚合计算,以供实时查询针对实时数据处理,一般使用 Storm 或者 Spack Streaming 来对链路数据进行实时聚合加工,存储一拜是用 OLTP 数据仓库,比如 HBase,使用 traceId 作为 RowKey,能天然地把一条调用链聚合在一起,提高查询效率。离线数据处理:要求计算效率相对没那么高,一般能在小时级别完成链路数据的聚合计算即可,一般用作汇总统计。针对离线数据处理,一般通过运行 MapReduce 或者 Spark 批处理程序来对链路数据进行离线计算,存储一般使用 Hive数据展示作用:将处理后的链路信息以图形化的方式展示给用户和做故障定位调用链路图(eg:Zipkin)服务整体情况:服务总耗时、服务调用的网络深度、每一层经过的系统,以及多少次调用。下图展示的一次调用,总耗时 209.323ms,经过了 5个不同系统模块,调用深度为 7 层,共发生了 2调用拓扑图(Pinpoint)调用拓扑图是一种全局视野,在实际项目中,主要用作全局监控,用户发现系统异常的点,从而快速做出决策。比如,某一个服务突然出现异常,那么在调用链路拓扑图中可以看出对这个服务的调用耗时都变高了,可以用红色的图样标出来,用作监控报警。总结服务追踪能够帮助查询一次用户请求在系统中的具体执行路径,以及每一条路径下的上下游的详细情况,对于追查问题十分有用。实现一个服务追踪系统,设计数据采集、数据处理和数据展示三个流程,有多种实现方式,具体采取某一种要根据自己的业务情况来选择。6、微服务治理的手段有哪些一次服务调用,服务提供者、注册中心、网络这三者都可能会有问题,此时服务消费者应该如何处理才能确保调用成功呢?这就是服务治理要解决的问题。节点管理服务调用失败一般是由两类原因引起的服务提供者自身出现问题,比如服务器宕机、进程意外退出等网络问题,如服务提供者、注册中心、服务消费者这三者任意两者之间的网络问题 无论是服务哪种原因,都有两种节点管理手段:注册中心主动摘除机制这种机制要求服务提供者定时的主动向注册中心汇报心跳,注册中心根据服务提供者节点最近一次汇报心跳的时间与上一次汇报心跳时间做比较,如果超出一定时间,就认为服务提供者出现问题,继而把节点从服务列表中摘除,并把最近的可用服务节点列表推送给服务消费者。服务消费者摘除机制虽然注册中心主动摘除机制可以解决服务提供者节点异常的问题,但如果是因为注册中心与服务提供者之间的网络出现异常,最坏的情况是注册中心会把服务节点全部摘除,导致服务消费者没有可能的服务节点调用,但其实这时候提供者本身是正常的。所以,将存活探测机制用在服务消费者这一端更合理,如果服务消费者调用服务提供者节点失败,就将这个节点从内存保存的可用夫提供者节点列表一处。负载均衡算法常用的负载均衡算法主要包括以下几种:随机算法(均匀)轮询算法(按照固定的权重,对可用服务节点进行轮询)最少活跃调用算法(性能理论最优)一致性 Hash 算法(相同参数的请求总是发到同一服务节点)服务路由对于服务消费者而言,在内存中的可用服务节点列表中选择哪个节点不仅由负载均衡算法决定,还由路由规则决定。所谓的路由规则,就是通过一定的规则如条件表达式或者正则表达式来限定服务节点的选择范围。为什么要指定路由规则呢?主要有两个原因:业务存在灰度发布的需求比如,服务提供者做了功能变更,但希望先只让部分人群使用,然后根据这部分人群的使用反馈,再来决定是否全量发布。多机房就近访问的需求跨数据中心的调用视距离远近都会有一定的网络延迟,像北京和广州这种几千公里距离的网络延迟可能达到了30ms以上,这对于有些业务几乎是不可接受的,所以就要一次服务调用尽量选择同一个 IDC 内部节点,从而减少网络耗时开销,提高性能。这时一般可以通过 IP 段规则来控制访问,在选择服务节点时,优先选择同一 IP段的节点。那么路由规则该如何配置?静态配置:服务消费者本地存放调用的路由规则,如果改变,需重新上线才能生效动态配置:路由规则存放在配置中心,服务消费者定期去请求注册中心来保持同步,要想改变消费者的路由配置,可以通过修改注册中心的配置,服务消费者在下一个同步周期之后,就会请求注册中心更新配置,从而实现动态变更服务容错常用的手段主要有以下几种:FailOver:失败自动切换(调用失败或者超时,可以设置重试次数)FailBack:失败通知(调用失败或者超时,不立即发起重试,而是根据失败的详细信息,来决定后续的执行策略)FailCache:失败缓存(调用失败或者超时,不立即发起重试,而是隔一段时间后再次尝试发起调用)FailFirst:快速失败(调用一次失败后,不再充实,一般非核心业务的调用,会采取快速失败策略,调用失败后一般就记录下失败日志就返回了)一般对于幂等的调用可以选择 FailOver 或者 FailCache,非幂等的调用可以选择 Failback 或者 FailFast在此我向大家推荐一个架构学习交流群。交流学习群号:478030634 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多 总结节点管理是从服务节点健康状态角度来考虑,负载均衡和服务路由是从服务节点访问优先级角度来考虑,而服务容错是从调用的健康状态来考虑,可谓殊途同归。在实际的微服务架构中,上面的服务治理手段一般都会在服务框架中默认即成,比如 阿里的 Dubbo、微博开源的服务架构 Motan等。原文出处:https://my.oschina.net/rosett… ...

October 21, 2018 · 2 min · jiezi

【Nginx源码研究】初探nginx HTTP处理流程

运营研发团队 李乐1.初始化服务器server指令用于配置virtual server,我们通常会在一台机器配置多个virtual server,监听不同端口号,映射到不同文件目录;nginx解析用户配置,在所有端口创建socket并启动监听。nginx解析配置文件是由各个模块分担处理的,每个模块注册并处理自己关心的配置,通过模块结构体ngx_module_t的字段ngx_command_t *commands实现;例如ngx_http_module是一个核心模块,其commands字段定义如下:struct ngx_command_s { ngx_str_t name; ngx_uint_t type; char *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);}; static ngx_command_t ngx_http_commands[] = { { ngx_string(“http”), NGX_MAIN_CONF|NGX_CONF_BLOCK|NGX_CONF_NOARGS, ngx_http_block, },};name指令名称,解析配置文件时按照名称能匹配查找;type指令类型,NGX_CONF_NOARGS该配置无参数,NGX_CONF_BLOCK该配置是一个配置块,NGX_MAIN_CONF表示配置可以出现在哪些位(NGX_MAIN_CONF、NGX_HTTP_SRV_CONF、NGX_HTTP_LOC_CONF);set指令处理函数指针;可以看到解析http指令的处理函数为ngx_http_block,实现如下:static char * ngx_http_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf){ //解析main配置 //解析server配置 //解析location配置 //初始化HTTP处理流程所需的handler //初始化listening if (ngx_http_optimize_servers(cf, cmcf, cmcf->ports) != NGX_OK) { return NGX_CONF_ERROR; }}ngx_http_optimize_servers方法循环所有配置端口,创建ngx_listening_t对象,并将其添加到conf->cycle->listening(后续操作会遍历此数组,创建socket并监听)。方法主要操作如下图:注意到这里设置了ngx_listening_t的handler为ngx_http_init_connection,当接收到socket连接请求时,会调用此handler处理。那么什么时候启动监听呢?全局搜索关键字cycle->listening可以找到。main方法会调用ngx_init_cycle,其完成了服务器初始化的大部分工作,其中就包括启动监听(ngx_open_listening_sockets)。假设nginx使用epoll处理所有socket事件,什么时候将监听事件添加到epoll呢?全局搜索关键字cycle->listening可以找到。ngx_event_core_module模块是事件处理核心模块,初始化此模块时会执行ngx_event_process_init函数,其中将监听事件添加到epoll。static ngx_int_t ngx_event_process_init(ngx_cycle_t *cycle){ ls = cycle->listening.elts; for (i = 0; i < cycle->listening.nelts; i++) { //设置读事件处理handler rev->handler = ngx_event_accept; ngx_add_event(rev, NGX_READ_EVENT, 0); }}注意到接收到客户端socket连接请求事件的处理函数是ngx_event_accept。2.HTTP请求解析2.1 基础结构体结构体ngx_connection_t存储socket连接相关信息;nginx预先创建若干个ngx_connection_t对象,存储在全局变量ngx_cycle->free_connections,称之为连接池;当新生成socket时,会尝试从连接池中获取空闲connection连接,如果获取失败,则会直接关闭此socket。指令worker_connections用于配置连接池最大连接数目,配置在events指令块中,由ngx_event_core_module解析。vents { use epoll; worker_connections 60000;}当nginx作为HTTP服务器时,最大客户端数目maxClient=worker_processesworker_connections/2;当nginx作为反向代理服务器时,最大客户端数目maxClient=worker_processesworker_connections/4。其worker_processes为用户配置的worker进程数目。结构体ngx_connection_t定义如下:struct ngx_connection_s { //空闲连接池中,data指向下一个连接,形成链表;取出来使用时,data指向请求结构体ngx_http_request_s void *data; //读写事件结构体,两个关键字段:handler处理函数、timer定时器 ngx_event_t *read; ngx_event_t *write; ngx_socket_t fd; //socket fd ngx_recv_pt recv; //socket接收数据函数指针 ngx_send_pt send; //socket发送数据函数指针 ngx_buf_t *buffer; //输入缓冲区 struct sockaddr *sockaddr; //客户端地址 socklen_t socklen; ngx_listening_t *listening; //监听的ngx_listening_t对象 struct sockaddr *local_sockaddr; //本地地址 socklen_t local_socklen; …………}结构体ngx_http_request_t存储整个HTTP请求处理流程所需的所有信息,字段非常多,这里只进行简要说明:struct ngx_http_request_s { ngx_connection_t *connection; //读写事件处理handler ngx_http_event_handler_pt read_event_handler; ngx_http_event_handler_pt write_event_handler; //请求头缓冲区 ngx_buf_t *header_in; //解析后的请求头 ngx_http_headers_in_t headers_in; //请求体结构体 ngx_http_request_body_t *request_body; //请求行 ngx_str_t request_line; //解析后请求行若干字段 ngx_uint_t method; ngx_uint_t http_version; ngx_str_t uri; ngx_str_t args; …………}请求行与请求体解析相对比较简单,这里重点讲述请求头的解析,解析后的请求头信息都存储在ngx_http_headers_in_t结构体中。ngx_http_request.c文件中定义了所有的HTTP头部,存储在ngx_http_headers_in数组,数组的每个元素是一个ngx_http_header_t结构体,主要包含三个字段,头部名称、头部解析后字段存储在ngx_http_headers_in_t的偏移量,解析头部的处理函数。ngx_http_header_t ngx_http_headers_in[] = { { ngx_string(“Host”), offsetof(ngx_http_headers_in_t, host), ngx_http_process_host }, { ngx_string(“Connection”), offsetof(ngx_http_headers_in_t, connection), ngx_http_process_connection }, …………} typedef struct { ngx_str_t name; ngx_uint_t offset; ngx_http_header_handler_pt handler;} ngx_http_header_t;解析请求头时,从ngx_http_headers_in数组中查找请求头ngx_http_header_t对象,调用处理函数handler,存储到r->headers_in对应字段。以解析Connection头部为例,ngx_http_process_connection实现如下:static ngx_int_t ngx_http_process_connection(ngx_http_request_t *r, ngx_table_elt_t *h, ngx_uint_t offset){ if (ngx_strcasestrn(h->value.data, “close”, 5 - 1)) { r->headers_in.connection_type = NGX_HTTP_CONNECTION_CLOSE; } else if (ngx_strcasestrn(h->value.data, “keep-alive”, 10 - 1)) { r->headers_in.connection_type = NGX_HTTP_CONNECTION_KEEP_ALIVE; } return NGX_OK;}输入参数offset在此处并没有什么作用。注意到第二个输入参数ngx_table_elt_t,存储了当前请求头的键值对信息:typedef struct { ngx_uint_t hash; //请求头key的hash值 ngx_str_t key; ngx_str_t value; u_char *lowcase_key; //请求头key转为小写字符串(可以看到HTTP请求头解析时key不区分大小写)} ngx_table_elt_t;再思考一个问题,从ngx_http_headers_in数组中查找请求头对应ngx_http_header_t对象时,需要遍历,每个元素都需要进行字符串比较,效率低下。因此nginx将ngx_http_headers_in数组转换为哈希表,哈希表的键即为请求头的key,方法ngx_http_init_headers_in_hash实现了数组到哈希表的转换,转换后的哈希表存储在cmcf->headers_in_hash字段。2.2 解析HTTP请求第1节提到,在创建socket启动监听时,会添加可读事件到epoll,事件处理函数为ngx_event_accept,用于接收socket连接,分配connection连接,并调用ngx_listening_t对象的处理函数(ngx_http_init_connection)。void ngx_event_accept(ngx_event_t *ev){ s = accept4(lc->fd, (struct sockaddr *) sa, &socklen, SOCK_NONBLOCK); //客户端socket连接成功时,都需要分配connection连接,如果分配失败则会直接关闭此socket。 //而每个worker进程连接池的最大连接数目是固定的,当不存在空闲连接时,此worker进程accept的所有socket都会被拒绝; //多个worker进程通过竞争执行epoll_wait;而当ngx_accept_disabled大于0时,会直接放弃此次竞争,同时ngx_accept_disabled减1。 //以此实现,当worker进程的空闲连接过少时,减少其竞争epoll_wait次数 ngx_accept_disabled = ngx_cycle->connection_n / 8 - ngx_cycle->free_connection_n; c = ngx_get_connection(s, ev->log); ls->handler(c);}socket连接成功后,nginx会等待客户端发送HTTP请求,默认会有60秒的超时时间,即60秒内没有接收到客户端请求时,断开此连接,打印错误日志。函数ngx_http_init_connection用于设置读事件处理函数,以及超时定时器。void ngx_http_init_connection(ngx_connection_t *c){ c->read = ngx_http_wait_request_handler; c->write->handler = ngx_http_empty_handler; ngx_add_timer(rev, c->listening->post_accept_timeout);}全局搜索post_accept_timeout字段,可以查找到设置此超时时间的配置指令,client_header_timeout,其可以在http、server指令块中配置。函数ngx_http_wait_request_handler为解析HTTP请求的入口函数,实现如下:static void ngx_http_wait_request_handler(ngx_event_t *rev){ //读事件已经超时 if (rev->timedout) { ngx_log_error(NGX_LOG_INFO, c->log, NGX_ETIMEDOUT, “client timed out”); ngx_http_close_connection(c); return; } size = cscf->client_header_buffer_size; //client_header_buffer_size指令用于配置接收请求头缓冲区大小 b = c->buffer; n = c->recv(c, b->last, size); //创建请求对象ngx_http_request_t,HTTP请求整个处理过程都有用; c->data = ngx_http_create_request(c); rev->handler = ngx_http_process_request_line; //设置读事件处理函数(此次请求行可能没有读取完) ngx_http_process_request_line(rev);}函数ngx_http_create_request创建并初始化ngx_http_request_t对象,注意这赋值语句r->header_in =c->buffer。解析请求行与请求头的代码较为繁琐,终点在于读取socket数据,解析字符串,这里不做详述。HTTP请求解析过程主要函数调用如下图所示:注意,解析完成请求行与请求头,nginx就开始处理HTTP请求,并没有等到解析完请求体再处理。处理请求入口为ngx_http_process_request。3.处理HTTP请求3.1 HTTP请求处理的11个阶段nginx将HTTP请求处理流程分为11个阶段,绝大多数HTTP模块都会将自己的handler添加到某个阶段(将handler添加到全局唯一的数组phases中),注意其中有4个阶段不能添加自定义handler,nginx处理HTTP请求时会挨个调用每个阶段的handler;typedef enum { NGX_HTTP_POST_READ_PHASE = 0, //第一个阶段,目前只有realip模块会注册handler,但是该模块默认不会运行(nginx作为代理服务器时有用,后端以此获取客户端原始ip) NGX_HTTP_SERVER_REWRITE_PHASE, //server块中配置了rewrite指令,重写url NGX_HTTP_FIND_CONFIG_PHASE, //查找匹配的location配置;不能自定义handler; NGX_HTTP_REWRITE_PHASE, //location块中配置了rewrite指令,重写url NGX_HTTP_POST_REWRITE_PHASE, //检查是否发生了url重写,如果有,重新回到FIND_CONFIG阶段;不能自定义handler; NGX_HTTP_PREACCESS_PHASE, //访问控制,比如限流模块会注册handler到此阶段 NGX_HTTP_ACCESS_PHASE, //访问权限控制,比如基于ip黑白名单的权限控制,基于用户名密码的权限控制等 NGX_HTTP_POST_ACCESS_PHASE, //根据访问权限控制阶段做相应处理;不能自定义handler; NGX_HTTP_TRY_FILES_PHASE, //只有配置了try_files指令,才会有此阶段;不能自定义handler; NGX_HTTP_CONTENT_PHASE, //内容产生阶段,返回响应给客户端 NGX_HTTP_LOG_PHASE //日志记录} ngx_http_phases;nginx使用结构体ngx_module_s表示一个模块,其中字段ctx,是一个指向模块上下文结构体的指针(上下文结构体的字段都是一些函数指针);nginx的HTTP模块上下文结构体大多都有字段postconfiguration,负责注册本模块的handler到某个处理阶段。11个阶段在解析完成http配置块指令后初始化。static char * ngx_http_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf){ //解析http配置块 //初始化11个阶段的phases数组,注意多个模块可能注册到同一个阶段,因此phases是一个二维数组 if (ngx_http_init_phases(cf, cmcf) != NGX_OK) { return NGX_CONF_ERROR; } //遍历索引HTTP模块,注册handler for (m = 0; ngx_modules[m]; m++) { if (ngx_modules[m]->type != NGX_HTTP_MODULE) { continue; } module = ngx_modules[m]->ctx; if (module->postconfiguration) { if (module->postconfiguration(cf) != NGX_OK) { return NGX_CONF_ERROR; } } } //将二维数组转换为一维数组,从而遍历执行数组所有handler if (ngx_http_init_phase_handlers(cf, cmcf) != NGX_OK) { return NGX_CONF_ERROR; }}以限流模块ngx_http_limit_req_module模块为例,postconfiguration方法简单实现如下:static ngx_int_t ngx_http_limit_req_init(ngx_conf_t cf){ h = ngx_array_push(&cmcf->phases[NGX_HTTP_PREACCESS_PHASE].handlers); h = ngx_http_limit_req_handler; //ngx_http_limit_req_module模块的限流方法;nginx处理HTTP请求时,都会调用此方法判断应该继续执行还是拒绝请求 return NGX_OK;}GDB调试,断点到ngx_http_block方法执行所有HTTP模块注册handler之后,打印phases数组p cmcf->phases[].handlersp (ngx_http_handler_pt)cmcf->phases[].handlers.elts11个阶段注册的handler如下图所示:3.2 11个阶段初始化上面提到HTTP的11个处理阶段handler存储在phases数组,但由于多个模块可能注册handler到同一个阶段,使得phases是一个二维数组,因此需要转换为一维数组,转换后存储在cmcf->phase_engine字段,phase_engine的类型为ngx_http_phase_engine_t,定义如下:typedef struct { ngx_http_phase_handler_t *handlers; //一维数组,存储所有handler ngx_uint_t server_rewrite_index; //记录NGX_HTTP_SERVER_REWRITE_PHASE阶段handler的索引值 ngx_uint_t location_rewrite_index; //记录NGX_HTTP_REWRITE_PHASE阶段handler的索引值} ngx_http_phase_engine_t; struct ngx_http_phase_handler_t { ngx_http_phase_handler_pt checker; //执行handler之前的校验函数 ngx_http_handler_pt handler; ngx_uint_t next; //下一个待执行handler的索引(通过next实现handler跳转执行)}; //cheker函数指针类型定义typedef ngx_int_t (*ngx_http_phase_handler_pt)(ngx_http_request_t *r, ngx_http_phase_handler_t *ph);//handler函数指针类型定义typedef ngx_int_t (*ngx_http_handler_pt)(ngx_http_request_t *r);数组转换函数ngx_http_init_phase_handlers实现如下:static ngx_int_t ngx_http_init_phase_handlers(ngx_conf_t *cf, ngx_http_core_main_conf_t cmcf){ use_rewrite = cmcf->phases[NGX_HTTP_REWRITE_PHASE].handlers.nelts ? 1 : 0; use_access = cmcf->phases[NGX_HTTP_ACCESS_PHASE].handlers.nelts ? 1 : 0; n = use_rewrite + use_access + cmcf->try_files + 1 / find config phase */; //至少有4个阶段,这4个阶段是上面说的不能注册handler的4个阶段 //计算handler数目,分配空间 for (i = 0; i < NGX_HTTP_LOG_PHASE; i++) { n += cmcf->phases[i].handlers.nelts; } ph = ngx_pcalloc(cf->pool, n * sizeof(ngx_http_phase_handler_t) + sizeof(void *)); //遍历二维数组 for (i = 0; i < NGX_HTTP_LOG_PHASE; i++) { h = cmcf->phases[i].handlers.elts; switch (i) { case NGX_HTTP_SERVER_REWRITE_PHASE: if (cmcf->phase_engine.server_rewrite_index == (ngx_uint_t) -1) { cmcf->phase_engine.server_rewrite_index = n; //记录NGX_HTTP_SERVER_REWRITE_PHASE阶段handler的索引值 } checker = ngx_http_core_rewrite_phase; break; case NGX_HTTP_FIND_CONFIG_PHASE: find_config_index = n; //记录NGX_HTTP_FIND_CONFIG_PHASE阶段的索引,NGX_HTTP_POST_REWRITE_PHASE阶段可能会跳转回此阶段 ph->checker = ngx_http_core_find_config_phase; n++; ph++; continue; //进入下一个阶段NGX_HTTP_REWRITE_PHASE case NGX_HTTP_REWRITE_PHASE: if (cmcf->phase_engine.location_rewrite_index == (ngx_uint_t) -1) { cmcf->phase_engine.location_rewrite_index = n; //记录NGX_HTTP_REWRITE_PHASE阶段handler的索引值 } checker = ngx_http_core_rewrite_phase; break; case NGX_HTTP_POST_REWRITE_PHASE: if (use_rewrite) { ph->checker = ngx_http_core_post_rewrite_phase; ph->next = find_config_index; n++; ph++; } continue; //进入下一个阶段NGX_HTTP_ACCESS_PHASE case NGX_HTTP_ACCESS_PHASE: checker = ngx_http_core_access_phase; n++; break; case NGX_HTTP_POST_ACCESS_PHASE: if (use_access) { ph->checker = ngx_http_core_post_access_phase; ph->next = n; ph++; } continue; //进入下一个阶段 case NGX_HTTP_TRY_FILES_PHASE: if (cmcf->try_files) { ph->checker = ngx_http_core_try_files_phase; n++; ph++; } continue; case NGX_HTTP_CONTENT_PHASE: checker = ngx_http_core_content_phase; break; default: checker = ngx_http_core_generic_phase; } //n为下一个阶段第一个handler的索引 n += cmcf->phases[i].handlers.nelts; //遍历当前阶段的所有handler for (j = cmcf->phases[i].handlers.nelts - 1; j >=0; j–) { ph->checker = checker; ph->handler = h[j]; ph->next = n; ph++; } }}GDB打印出转换后的数组如下图所示,第一列是cheker字段,第二列是handler字段,箭头表示next跳转;图中有个返回的箭头,即NGX_HTTP_POST_REWRITE_PHASE阶段可能返回到NGX_HTTP_FIND_CONFIG_PHASE;原因在于只要NGX_HTTP_REWRITE_PHASE阶段产生了url重写,就需要重新查找匹配location。3.3 处理HTTP请求2.2节提到HTTP请求的处理入口函数是ngx_http_process_request,其主要调用ngx_http_core_run_phases实现11个阶段的执行流程;ngx_http_core_run_phases遍历预先设置好的cmcf->phase_engine.handlers数组,调用其checker函数,逻辑如下:void ngx_http_core_run_phases(ngx_http_request_t *r){ ph = cmcf->phase_engine.handlers; //phase_handler初始为0,表示待处理handler的索引;cheker内部会根据ph->next字段修改phase_handler while (ph[r->phase_handler].checker) { rc = ph[r->phase_handler].checker(r, &ph[r->phase_handler]); if (rc == NGX_OK) { return; } }}checker内部就是调用handler,并设置下一步要执行handler的索引;比如说ngx_http_core_generic_phase实现如下:ngx_int_t ngx_http_core_generic_phase(ngx_http_request_t *r, ngx_http_phase_handler_t *ph){ ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, “rewrite phase: %ui”, r->phase_handler); rc = ph->handler(r); if (rc == NGX_OK) { r->phase_handler = ph->next; return NGX_AGAIN; }}3.4 内容产生阶段内容产生阶段NGX_HTTP_CONTENT_PHASE是HTTP请求处理的第10个阶段,一般情况有3个模块注册handler到此阶段:ngx_http_static_module、ngx_http_autoindex_module和ngx_http_index_module。但是当我们配置了proxy_pass和fastcgi_pass时,情况会有所不同;使用proxy_pass配置上游时,ngx_http_proxy_module模块会设置其处理函数到配置类conf;使用fastcgi_pass配置时,ngx_http_fastcgi_module会设置其处理函数到配置类conf。例如:static char * ngx_http_fastcgi_pass(ngx_conf_t *cf, ngx_command_t *cmd, void *conf){ ngx_http_core_loc_conf_t *clcf; clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module); clcf->handler = ngx_http_fastcgi_handler;}阶段NGX_HTTP_FIND_CONFIG_PHASE查找匹配的location,并获取此ngx_http_core_loc_conf_t对象,将其handler赋值给ngx_http_request_t对象的content_handler字段(内容产生处理函数)。而在执行内容产生阶段的checker函数时,会执行content_handler指向的函数;查看ngx_http_core_content_phase函数实现(内容产生阶段的checker函数):ngx_int_t ngx_http_core_content_phase(ngx_http_request_t *r, ngx_http_phase_handler_t *ph){ if (r->content_handler) { //如果请求对象的content_handler字段不为空,则调用 r->write_event_handler = ngx_http_request_empty_handler; ngx_http_finalize_request(r, r->content_handler(r)); return NGX_OK; } ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, “content phase: %ui”, r->phase_handler); rc = ph->handler(r); //否则执行内容产生阶段handler}总结nginx处理HTTP请求的流程较为复杂,因此本文只是简单提供了一条线索:分析了nginx服务器启动监听的过程,HTTP请求的解析过程,11个阶段的初始化与调用过程。至于HTTP解析处理的详细流程,还需要读者去探索。 ...

October 16, 2018 · 4 min · jiezi

【Nginx源码研究】Master进程浅析

运营研发团队 季伟滨一、前言众所周如,Nginx是多进程架构。有1个master进程和N个worker进程,一般N等于cpu的核数。另外, 和文件缓存相关,还有cache manager和cache loader进程。 master进程并不处理网络请求,网络请求是由worker进程来处理,而master进程负责管理这些worker进程。比如当一个worker进程意外挂掉了,他负责拉起新的worker进程,又比如通知所有的worker进程平滑的退出等等。本篇wiki将简单分析下master进程是如何做管理工作的。二、nginx进程模式在开始讲解master进程之前,我们需要首先知道,其实Nginx除了生产模式(多进程+daemon)之外,还有其他的进程模式,虽然这些模式一般都是为了研发&调试使用。非daemon模式以非daemon模式启动的nginx进程并不会立刻退出。其实在终端执行非bash内置命令,终端进程会fork一个子进程,然后exec执行我们的nginx bin。然后终端进程本身会进入睡眠态,等待着子进程的结束。在nginx的配置文件中,配置【daemon off;】即可让进程模式切换到前台模式。下图展示了一个测试例子,将worker的个数设置为1,开启非daemon模式,开启2个终端pts/0和pts/1。在pts/1上执行nginx,然后在pts/0上看进程的状态,可以看到终端进程进入了阻塞态(睡眠态)。这种情况下启动的master进程,它的父进程是当前的终端进程(/bin/bash),随着终端的退出(比如ctrl+c),所有nginx进程都会退出。single模式nginx可以以单进程的形式对外提供完整的服务。这里进程可以是daemon,也可以不是daemon进程,都没有关系。在nginx的配置文件中,配置【master_process off;】即可让进程模式切换到单进程模式。这时你会看到,只有一个进程在对外服务。生产模式(多进程+daemon)想像一下一般我们是怎么启动nginx的,我在自己的vm上把Nginx安装到了/home/xiaoju/nginx-jiweibin,所以启动命令一般是这样:/home/xiaoju/nginx-jiweibin/sbin/nginx然后,ps -ef|grep nginx就会发现启动好了master和worker进程,像下面这样(warn是由于我修改worker_processes为1,但未修改worker_cpu_affinity,可以忽略)这里和非daemon模式的一个很大区别是启动程序(终端进程的子进程)会立刻退出,并被终端进程这个父进程回收。同时会产生master这种daemon进程,可以看到master进程的父进程id是1,也就是init或systemd进程。这样,随着终端的退出,master进程仍然可以继续服务,因为master进程已经和启动nginx命令的终端shell进程无关了。 启动nginx命令,是如何生成daemon进程并退出的呢?答案很简单,同样是fork系统调用。它会复制一个和当前启动进程具有相同代码段、数据段、堆和栈、fd等信息的子进程(尽管cow技术使得复制发生在需要分离那一刻),参见图-1。图1-生产模式Nginx进程启动示意图三、master执行流程master进程被fork后,继续执行ngx_master_process_cycle函数。这个函数主要进行如下操作:1、设置进程的初始信号掩码,屏蔽相关信号2、fork子进程,包括worker进程和cache manager进程、cache loader进程3、进入主循环,通过sigsuspend系统调用,等待着信号的到来。一旦信号到来,会进入信号处理程序。信号处理程序执行之后,程序执行流程会判断各种状态位,来执行不同的操作。图2- ngx_master_process_cycle执行流程示意图四、信号介绍master进程的主循环里面,一直通过等待各种信号事件,来处理不同的指令。这里先普及信号的一些知识,有了这些知识的铺垫再看master相关代码会更加从容一些(如果对信号比较熟悉,可以略过这一节)。标准信号和实时信号信号分为标准信号(不可靠信号)和实时信号(可靠信号),标准信号是从1-31,实时信号是从32-64。一般我们熟知的信号比如,SIGINT,SIGQUIT,SIGKILL等等都是标准信号。master进程监听的信号也是标准信号。标准信号和实时信号有一个区别就是:标准信号,是基于位的标记,假设在阻塞等待的时候,多个相同的信号到来,最终解除阻塞时,只会传递一次信号,无法统计等待期间信号的计数。而实时信号是通过队列来实现,所以,假设在阻塞等待的时候,多个相同的信号到来,最终解除阻塞的时候,会传递多次信号。信号处理器信号处理器是指当捕获指定信号时(传递给进程)时将会调用的一个函数。信号处理器程序可能随时打断进程的主程序流程。内核代表进程来执行信号处理器函数,当处理器返回时,主程序会在处理器被中断的位置恢复执行。(主程序在执行某一个系统调用的时候,有可能被信号打断,当信号处理器返回时,可以通过参数控制是否重启这个系统调用)。信号处理器函数的原型是:void (* sighandler_t)(int);入参是1-31的标准信号的编号。比如SIGHUP的编号是1,SIGINT的编号是2。通过sigaction调用可以对某一个信号安装信号处理器。函数原型是:int sigaction(int sig,const struct sigaction act,struct sigaction oldact); sig表示想要监听的信号。act是监听的动作对象,这里包含信号处理器的函数指针,oldact是指之前的信号处理器信息。见下面的结构体定义:struct sigaction{ void (*sa_handler)(int); sigset_t sa_mask; int sa_flags; void (*sa_restorer)(void); }sa_hander就是我们的信号处理器函数指针。除了捕获信号外,进程对信号的处理还可以有忽略该信号(使用SIG_IGN常量)和执行缺省操作(使用SIG_DFL常量)。这里需要注意,SIGKILL信号和SIGSTOP信号不能被捕获、阻塞、忽略的。sa_mask是一组信号,在sa_handler执行期间,会将这组信号加入到进程信号掩码中(进程信号掩码见下面描述),对于在sa_mask中的信号,会保持阻塞。sa_flags包含一些可以改变处理器行为的标记位,比如SA_NODEFER表示执行信号处理器时不自动将该信号加入到信号掩码 SA_RESTART表示自动重启被信号处理器中断的系统调用。sa_restorer仅内部使用,应用程序很少使用。发送信号一般我们给某个进程发送信号,可以使用kill这个shell命令。比如kill -9 pid,就是发送SIGKILL信号。kill -INT pid,就可以发送SIGINT信号给进程。与shell命令类似,可以使用kill系统调用来向进程发送信号。函数原型是:(注意,这里发送的一般都是标准信号,实时信号使用sigqueue系统调用来发送)。int kill(pit_t pid, int sig); 另外,子进程退出,会自动给父进程发送SIGCHLD信号,父进程可以监听这一信号来满足相应的子进程管理,如自动拉起新的子进程。进程信号掩码内核会为每个进程维护一个信号掩码。信号掩码包含一组信号,对于掩码中的信号,内核会阻塞其对进程的传递。信号被阻塞后,对信号的传递会延后,直到信号从掩码中移除。假设通过sigaction函数安装信号处理器时不指定SA_NODEFER这个flag,那么执行信号处理器时,会自动将捕获到的信号加入到信号掩码,也就是在处理某一个信号时,不会被相同的信号中断。通过sigprocmask系统调用,可以显式的向信号掩码中添加或移除信号。函数原型是:int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);how可以使下面3种:SIG_BLOCK:将set指向的信号集内的信号添加到信号掩码中。即信号掩码是当前值和set的并集。SIG_UNBLOCK:将set指向的信号集内的信号从信号掩码中移除。SIG_SETMASK:将信号掩码赋值为set指向的信号集。等待信号在应用开发中,可能需要存在这种业务场景:进程需要首先屏蔽所有的信号,等相应工作已经做完之后,解除阻塞,然后一直等待着信号的到来(在阻塞期间有可能并没有信号的到来)。信号一旦到来,再次恢复对信号的阻塞。linux编程中,可以使用int pause(void)系统调用来等待信号的到来,该调用会挂起进程,直到信号到来中断该调用。基于这个调用,对于上面的场景可以编写下面的伪代码:struct sigaction sa;sigset_t initMask,prevMask; sigemptyset(&sa.sa_mask);sa.sa_flags = 0;sa.sa_handler = handler; sigaction(SIGXXX,&sa,NULL); //1-安装信号处理器 sigemptyset(&initMask);sigaddset(&initMask,xxx);sigaddset(&initMask,yyy);…. sigprocmask(SIG_BLOCK,&initMask,&prevMask); //2-设置进程信号掩码,屏蔽相关信号 do_something() //3-这段逻辑不会被信号所打扰 sigprocmask(SIG_SETMASK,&prevMask,NULL); //4-解除阻塞 pause(); //5-等待信号 sigprocmask(SIG_BLOCK,&initMask,&prevMask); //6-再次设置掩码,阻塞信号的传递 do_something2(); //7-这里一般需要监控一些全局标记位是否已经改变,全局标记位在信号处理器中被设置想想上面的代码会有什么问题?假设某一个信号,在上面的4之后,5之前到来,也就是解除阻塞之后,等待信号调用之前到来,信号会被信号处理器所处理,并且pause调用会一直陷入阻塞,除非有第二个信号的到来。这和我们的预期是不符的。这个问题本质是,解除阻塞和等待信号这2步操作不是原子的,出现了竞态条件。这个竞态条件发生在主程序和信号处理器对同一个被解除信号的竞争关系。要避免这个问题,可以通过sigsuspend调用来等待信号。函数原型是:int sigsuspend(const sigset_t mask);它接收一个掩码参数mask,用mask替换进程的信号掩码,然后挂起进程的执行,直到捕获到信号,恢复进程信号掩码为调用前的值,然后调用信号处理器,一旦信号处理器返回,sigsuspend将返回-1,并将errno置为EINTR五、基于信号的事件架构master进程启动之后,就会处于挂起状态。它等待着信号的到来,并处理相应的事件,如此往复。本节让我们看下nginx是如何基于信号构建事件监听框架的。安装信号处理器在nginx.c中的main函数里面,初始化进程fork master进程之前,就已经通过调用ngx_init_signals函数安装好了信号处理器,接下来fork的master以及work进程都会继承这个信号处理器。让我们看下源代码:/ @src/core/nginx.c */ int ngx_cdeclmain(int argc, char *const argv){ …. cycle = ngx_init_cycle(&init_cycle); … if (ngx_init_signals(cycle->log) != NGX_OK) { //安装信号处理器 return 1; } if (!ngx_inherited && ccf->daemon) { if (ngx_daemon(cycle->log) != NGX_OK) { //fork master进程 return 1; } ngx_daemonized = 1; } …} / @src/os/unix/ngx_process.c */ typedef struct { int signo; char *signame; char *name; void (*handler)(int signo);} ngx_signal_t; ngx_signal_t signals[] = { { ngx_signal_value(NGX_RECONFIGURE_SIGNAL), “SIG” ngx_value(NGX_RECONFIGURE_SIGNAL), “reload”, ngx_signal_handler }, … { SIGCHLD, “SIGCHLD”, “”, ngx_signal_handler }, { SIGSYS, “SIGSYS, SIG_IGN”, “”, SIG_IGN }, { SIGPIPE, “SIGPIPE, SIG_IGN”, “”, SIG_IGN }, { 0, NULL, “”, NULL }}; ngx_int_tngx_init_signals(ngx_log_t *log){ ngx_signal_t *sig; struct sigaction sa; for (sig = signals; sig->signo != 0; sig++) { ngx_memzero(&sa, sizeof(struct sigaction)); sa.sa_handler = sig->handler; sigemptyset(&sa.sa_mask); if (sigaction(sig->signo, &sa, NULL) == -1) {#if (NGX_VALGRIND) ngx_log_error(NGX_LOG_ALERT, log, ngx_errno, “sigaction(%s) failed, ignored”, sig->signame);#else ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, “sigaction(%s) failed”, sig->signame); return NGX_ERROR;#endif } } return NGX_OK;}全局变量signals是ngx_signal_t的数组,包含了nginx进程(master进程和worker进程)监听的所有的信号。ngx_signal_t有4个字段,signo表示信号的编号,signame表示信号的描述字符串,name在nginx -s时使用,用来作为向nginx master进程发送信号的快捷方式,例如nginx -s reload相当于向master进程发送一个SIGHUP信号。handler字段表示信号处理器函数指针。下面是针对不同的信号安装的信号处理器列表:通过上表,可以看到,在nginx中,只要捕获的信号,信号处理器都是ngx_signal_handler。ngx_signal_handler的实现细节将在后面进行介绍。设置进程信号掩码在ngx_master_process_cycle函数里面,fork子进程之前,master进程通过sigprocmask系统调用,设置了进程的初始信号掩码,用来阻塞相关信号。而对于fork之后的worker进程,子进程会继承信号掩码,不过在worker进程初始化的时候,对信号掩码又进行了重置,所以worker进程可以并不阻塞信号的传递。voidngx_master_process_cycle(ngx_cycle_t *cycle){ … sigset_t set; … sigemptyset(&set); sigaddset(&set, SIGCHLD); sigaddset(&set, SIGALRM); sigaddset(&set, SIGIO); sigaddset(&set, SIGINT); sigaddset(&set, ngx_signal_value(NGX_RECONFIGURE_SIGNAL)); sigaddset(&set, ngx_signal_value(NGX_REOPEN_SIGNAL)); sigaddset(&set, ngx_signal_value(NGX_NOACCEPT_SIGNAL)); sigaddset(&set, ngx_signal_value(NGX_TERMINATE_SIGNAL)); sigaddset(&set, ngx_signal_value(NGX_SHUTDOWN_SIGNAL)); sigaddset(&set, ngx_signal_value(NGX_CHANGEBIN_SIGNAL)); if (sigprocmask(SIG_BLOCK, &set, NULL) == -1) { ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno, “sigprocmask() failed”); } …挂起进程当做完上面2项准备工作后,就会进入主循环。在主循环里面,master进程通过sigsuspend系统调用,等待着信号的到来,在等待的过程中,进程一直处于挂起状态(S状态)。至此,master进程基于信号的整体事件监听框架讲解完成,关于信号到来之后的逻辑,我们在下一节讨论。voidngx_master_process_cycle(ngx_cycle_t *cycle){ …. if (sigprocmask(SIG_BLOCK, &set, NULL) == -1) { ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno, “sigprocmask() failed”); } sigemptyset(&set); //重置信号集合,作为后续sigsuspend入参,允许任何信号传递 … ngx_start_worker_processes(cycle, ccf->worker_processes, NGX_PROCESS_RESPAWN); //fork worker进程 ngx_start_cache_manager_processes(cycle, 0); //fork cache相关进程 … for ( ;; ) { … sigsuspend(&set); //挂起进程,等待信号 … //后续处理逻辑 } } //end of ngx_master_process_cycle六、主循环进程数据结构在展开说明之前,我们需要了解下,nginx对进程的抽象的数据结构。ngx_int_t ngx_last_process; //ngx_processes数组中有意义(当前有效或曾经有效)的进程,最大的下标+1(下标从0开始计算)ngx_process_t ngx_processes[NGX_MAX_PROCESSES]; //所有的子进程数组,NGX_MAX_PROCESSES为1024,也就是nginx子进程不能超过1024个。 typedef struct { ngx_pid_t pid; //进程pid int status; //进程状态,waitpid调用获取 ngx_socket_t channel[2]; //基于匿名socket的进程之间通信的管道,由socketpair创建,并通过fork复制给子进程。但一般是单向通信,channel[0]只用来写,channel[1]只用来读。 ngx_spawn_proc_pt proc; //子进程的循环方法,比如worker进程是ngx_worker_process_cycle void *data; //fork子进程后,会执行proc(cycle,data) char *name; //进程名称 unsigned respawn:1; //为1时表示受master管理的子进程,死掉可以复活 unsigned just_spawn:1; //为1时表示刚刚新fork的子进程,在重新加载配置文件时,会使用到 unsigned detached:1; //为1时表示游离的新的子进程,一般用在升级binary时,会fork一个新的master子进程,这时新master进程是detached,不受原来的master进程管理 unsigned exiting:1; //为1时表示正在主动退出,一般收到SIGQUIT或SIGTERM信号后,会置该值为1,区别于子进程的异常被动退出 unsigned exited:1; //为1时表示进程已退出,并通过waitpid系统调用回收} ngx_process_t;比如我只启动了一个worker进程,gdb master进程,ngx_processes和ngx_last_process的结果如图3所示:图3-gdb单worker进程下ngx_processes和ngx_last_process的结果全局标记上面我们提到ngx_signal_handler这个函数,它是nginx为捕获的信号安装的通用信号处理器。它都干了什么呢?很简单,它只是用来标记对应的全局标记位为1,这些标记位,后续的主循环里会使用到,根据不同的标记位,执行不同的逻辑。master进程对应的信号与全局标记位的对应关系如下表:对于SIGCHLD信号,情况有些复杂,ngx_signal_handler还会额外多做一件事,那就是调用ngx_process_get_status函数去做子进程的回收。在ngx_process_get_status内部,会使用waitpid系统调用获取子进程的退出状态,并回收子进程,避免产生僵尸进程。同时,会更新ngx_processes数组中相应的退出进程的exited为1,表示进程已退出,并被父进程回收。现在考虑一个问题:假设在进程屏蔽信号并且进行各种标记位的逻辑处理期间(下面会讲标记位的逻辑流程),同时有多个子进程退出,会产生多个SIGCHLD信号。但由于SIGCHLD信号是标准信号(非可靠信号),当sigsuspend等待信号时,只会被传递一个SIGCHLD信号。那么这样是否有问题呢?答案是否定的,因为ngx_process_get_status这里是循环的调用waitpid,所以在一个信号处理器的逻辑流程里面,会回收尽可能多的退出的子进程,并且更新ngx_processes中相应进程的exited标记位,因此不会存在漏掉的问题。static voidngx_process_get_status(void){ … for ( ;; ) { pid = waitpid(-1, &status, WNOHANG); if (pid == 0) { return; } if (pid == -1) { err = ngx_errno; if (err == NGX_EINTR) { continue; } if (err == NGX_ECHILD && one) { return; } … return; } … for (i = 0; i < ngx_last_process; i++) { if (ngx_processes[i].pid == pid) { ngx_processes[i].status = status; ngx_processes[i].exited = 1; process = ngx_processes[i].name; break; } } … }}逻辑流程主循环,针对不同的全局标记,执行不同action的整体逻辑流程见图4:图4-主循环逻辑流程上面的流程图,总体还是比较复杂的,根据具体的场景去分析会更加清晰一些。在此之前,下面先就图上一些需要描述的给予解释说明:1、临时变量live,它表示是否仍有存活的子进程。只有当ngx_processes中所有的子进程的exited标记位都为1时,live才等于0。而master进程退出的条件是【!live && (ngx_terminate || ngx_quit)】,即所有的子进程都已退出,并且接收到SIGTERM、SIGINT或者SIGQUIT信号时,master进程才会正常退出(通过SIGKILL信号杀死master一般在异常情况下使用,这里不算)。2、在循环的一开始,会判断delay是否大于0,这个delay其实只和ngx_terminate即强制退出的场景有关系。在后面会详细讲解。3、ngx_terminate、ngx_quit、ngx_reopen这3种标记,master进程都会通过上面提到的socket channel向子进程进行广播。如果写socket失败,会执行kill系统调用向子进程发送信号。而其他的case,master会直接执行kill系统调用向子进程发送信号,比如发送SIGKILL。关于socket channel,后续会进行讲解。4、除了和信号直接映射的标记位,我们看到,流程图中还有ngx_noaccepting和ngx_restart这2个全局标记位以及ngx_new_binary这个全局变量。ngx_noaccepting表示当前master下的所有的worker进程正在退出或已退出,不再对外服务。ngx_restart表示需要重新启动worker子进程,ngx_new_binary表示升级binary时新的master进程的pid,这3个都和升级binary有关系。socket channelnginx中进程之间通信的方式有多种,socket channel是其中之一。这种方式,不如共享内存使用的广泛,目前主要被使用在master进程广播消息到子进程,这里面的消息包括下面5种:#define NGX_CMD_OPEN_CHANNEL 1 //新建或者发布一个通信管道#define NGX_CMD_CLOSE_CHANNEL 2 //关闭一个通信管道#define NGX_CMD_QUIT 3 //平滑退出#define NGX_CMD_TERMINATE 4 //强制退出#define NGX_CMD_REOPEN 5 //重新打开文件master进程在创建子进程的时候,fork调用之前,会在ngx_processes中选择空闲的ngx_process_t,这个空闲的ngx_process_t的下标为s(s不超过1023)。然后通过socketpair调用创建一对匿名socket,相对应的fd存储在ngx_process_t的channel中。并且把s赋值给全局变量ngx_process_slot,把channel[1]赋值给全局变量ngx_channel。ngx_pid_tngx_spawn_process(ngx_cycle_t *cycle, ngx_spawn_proc_pt proc, void *data,char *name, ngx_int_t respawn) { …//寻找空闲的ngx_process_t,下标为s if (socketpair(AF_UNIX, SOCK_STREAM, 0, ngx_processes[s].channel) == -1) //创建匿名socket channel{ ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno, “socketpair() failed while spawning "%s"”, name); return NGX_INVALID_PID;}…ngx_channel = ngx_processes[s].channel[1];…ngx_process_slot = s;pid = fork(); //fork调用,子进程继承socket channel…fork之后,子进程继承了这对socket。因为他们共享了相同的系统级打开文件,这时master进程写channel[0],子进程就可以通过channel[1]读取到数据,master进程写channel[1],子进程就可以通过channel[0]读取到数据。子进程向master通信也是如此。这样在fork N个子进程之后,实际上会建立N个socket channel,如图5所示。图5-master和子进程通过socket channel通信原理在nginx中,对于socket channel的使用,总是使用channel[0]作为数据的发送端,channel[1]作为数据的接收端。并且master进程和子进程的通信是单向的,因此在后续子进程初始化时关闭了channel[0],只保留channel[1]即ngx_channel。同时将ngx_channel的读事件添加到整个nginx高效的事件框架中(关于事件框架这里限于篇幅不多谈),最终实现了master进程向子进程消息的同步。了解到这里,其实socket channel已经差不多了。但是还不是它的全部,nginx源码中还提供了通过socket channel进行子进程之间互相通信的机制。不过目前来看,没有实际的使用。让我们先思考一个问题:如果要实现worker之间的通信,难点在于什么?答案不难想到,master进程fork子进程是有顺序的,fork最后一个worker和master进程一样,知道所有的worker进程的channel[0],因此它可以像master一样和其他的worker通信。但是第一个worker就很糟糕了,它只知道自己的channel[0](而且还是被关闭了),也就是第一个worker无法主动向任意其他的woker进程通信。在图6中可以看到,对于第二个worker进程,仅仅知道第一个worker的channel[0],因此仅仅可以和第一个worker进行通信。图6-第二个worker进程的channel示意图nginx是怎么解决这个问题的呢?简单来讲, nginx使用了进程间传递文件描述符的技术。关于进程间传递文件描述符,这里关键的系统调用涉及到2个,socketpair和sendmsg,这里不细讲,有兴趣的可以参考下这篇文章:https://pureage.info/2015/03/…。master在每次fork新的worker的时候,都会通过ngx_pass_open_channel函数将新创建进程的pid以及的socket channel写端channel[0]传递给所有之前创建的worker。上面提到的NGX_CMD_OPEN_CHANNEL就是用来做这件事的。worker进程收到这个消息后,会解析消息的pid和fd,存储到ngx_processes中相应slot下的ngx_process_t中。这里channel[1]并没有被传递给子进程,因为channel[1]是接收端,每一个socket channel的channe[1]都唯一对应一个子进程,worker A持有worker B的channel[1],并没有任何意义。因此在子进程初始化时,会将之前worker进程创建的channel[1]全部关闭掉,只保留的自己的channel[1]。最终,如图7所示,每一个worker持有自己的channel的channel[1],持有着其他worker对应channel的channel[0]。而master则持有者所有的worker对应channel的channel[0]和channel[1](为什么这里master仍然保留着所有channel的channe[1],没有想明白为什么,也许是为了在未来监听worker进程的消息)。图7-socket channel最终示意图进程退出这里进程退出包含多种场景:1、worker进程异常退出2、系统管理员使用nginx -s stop或者nginx -s quit让进程全部退出3、系统管理员使用信号SIGINT,SIGTERM,SIGQUIT等让进程全部退出4、升级binary期间,新master进程退出(当发现重启的nginx有问题之后,可能会杀死新master进程)对于场景1,master进程需要重新拉起新的worker进程。对于场景2和3,master进程需要等到所有的子进程退出后再退出(避免出现孤儿进程)。对于场景4,本小节先不介绍,在后面会介绍binary升级。下面我们了解下master进程是如何实现前三个场景的。处理子进程退出子进程退出时,发送SIGCHLD信号给父进程,被信号处理器处理,会更新ngx_reap全局标记位,并且使用waitpid收集所有的子进程,设置ngx_processes中对应slot下的ngx_process_t中的exited为1。然后,在主循环中使用ngx_reap_children函数,对子进程退出进行处理。这个函数非常重要,是理解进程退出的关键。图8-ngx_reap_children函数流程图通过上图,可以看到ngx_reap_children函数的整体执行流程。它遍历ngx_processes数组里有效(pid不等于-1)的worker进程:一、如果子进程的exited标志位为1(即已退出并被master回收)1、如果子进程是游离进程(detached为1)1.1、如果退出的子进程是新master进程(升级binary时会fork一个新的master进程),会将旧的pid文件恢复,即恢复使用当前的master来服务【场景4】(1)如果当前master进程已经将它下面的worker都杀掉了(ngx_noaccepting为1),这时会修改全局标记位ngx_restart为1,然后跳到步骤1.c。在外层的主循环里,检测到这个标记位,master进程便会重新fork worker进程(2)如果当前的master进程还没有杀死他的子进程,直接跳到步骤1.c1.2、如果退出的子进程是其他进程,直接跳到步骤1.c(实际上这种case不存在,因为目前看,所有的detached的进程都是新master进程。detached只有在升级binary时才使用到)2、如果子进程不是游离进程(detached为0),通过socket channel通知其他的worker进程NGX_CMD_CLOSE_CHANNEL指令,管道需要关闭(我要死了,以后不用给我打电话了)2.1、如果子进程是需要复活的(进程标记respawn为1,并没有收到过相关退出信号),那么fork新的worker进程取代死掉的子进程,并通过socket channel通知其他的worker进程NGX_CMD_OPEN_CHANNEL指令,新的worker已启动,请记录好新启动进程的pid和channel[0](大家好,我是新worker xxx,这是我的电话,有事随时call me),同时置live为1,表示还有存活的子进程,master进程不可退出。然后继续遍历下一个进程【场景1】2.2、如果不需要复活,直接跳到步骤1.c【场景2+场景3】3、对于退出的进程,置ngx_process_t中的pid为-1,继续遍历下一个进程二、如果子进程exited标志为0,即没有退出1、如果子进程是非游离进程,那么更新live为1,然后继续遍历下一个进程。live为1表示还有存活的子进程,master进程不可退出(对这里的判断条件ngx_processes[i].exiting || !ngx_processes[i].detached存疑,大部分worker都是非游离,游离的进程只有升级 binary时的新master进程,但是新master退出时,并不会修改exiting为1,所以个人觉得这里的ngx_processes[i].exiting的判断没有必要,只需要判断是否游离进程即可)2、如果子进程是游离进程,那么忽略,遍历下一个进程。也就是说,master并不会因为游离子进程没有退出,而停止退出的步伐。(在这种case下,游离进程就像别人家的孩子一样,master不再关心)最终,ngx_reap_children会妥善的处理好各种场景的子进程退出,并且返回live的值。即告诉主循环,当前是否仍有存活的子进程存在。在主循环里,当!live && (ngx_terminate || ngx_quit)条件满足时,master进程就会做相应的进程退出工作(删除pid文件,调用每一个模块的exit_master函数,关闭监听的socket,释放内存池)。触发子进程退出对于场景2和场景3,当master进程收到SIGTERM或者SIGQUIT信号时,会在信号处理器中设置ngx_terminate或ngx_quit全局标记。当主循环检测到这2种标记时,会通过socket channel向所有的子进程广播消息,传递的指令分别是:NGX_CMD_TERMINATE或NGX_CMD_QUIT。子进程通过事件框架检测到该消息后,同样会设置ngx_terminate或者ngx_quit标记位为1(注意这里是子进程的全局变量)。子进程的主循环里检测到ngx_terminate时,会立即做进程退出工作(调用每一个模块的exit_process函数,释放内存池),而检测到ngx_quit时,情况会稍微复杂些,需要释放连接,关闭监听socket,并且会等待所有请求以及定时事件都被妥善的处理完之后,才会做进程退出工作。这里可能会有一个隐藏的问题:进程的退出可能没法被一次waitpid全部收集到,有可能有漏网之鱼还没有退出,需要等到下次的suspend才能收集到。如果按照上面的逻辑,可能存在重复给子进程发送退出指令的问题。nginx比较严谨,针对这个问题有自己的处理方式:ngx_quit:一旦给某一个worker进程发送了退出指令(强制退出或平滑退出),会记录该进程的exiting为1,表示这个进程正在退出。以后,如果还要再给该进程发送退出NGX_CMD_QUIT指令,一旦发现这个标记位为1,那么就忽略。这样就可以保证一次平滑退出,针对每一个worker只通知一次,不重复通知。ngx_terminate:和ngx_quit略有不同,它不依赖exiting标记位,而是通过sigio的临时变量(不是SIGIO信号)来缓解这个问题。在向worker进程广播NGX_CMD_TERMINATE之前,会置sigio为worker进程数+2(2个cache进程),每次信号到来(假设每次到来的信号都是SIGCHLD,并且只wait了一个子进程退出),sigio会减一。直到sigio为0,又会重新广播NGX_CMD_TERMINATE给worker进程。sigio大于0的期间,master是不会重复给worker发送指令的。(这里只是缓解,并没有完全屏蔽掉重复发指令的问题,至于为什么没有像ngx_quit一样处理,不是很明白这么设计的原因)ngx_terminate的timeout机制还记得上面提到的delay吗?这个变量只有在ngx_terminate为1时才大于0,那么它是用来干什么的?实际上,它用来在进程强制退出时做倒计时使用。master进程为了保证所有的子进程最终都会退出,会给子进程一定的时间,如果那时候仍有子进程没有退出,会直接使用SIGKILL信号杀死所有子进程。当最开始master进程处理ngx_terminate(第一次收到SIGTERM或者SIGINT信号)时,会将delay从0改为50ms。在下一个主循环的开始将设置一个时间为50ms的定时器。然后等待信号的到来。这时,子进程可能会陆续退出产生SIGCHLD信号。理想的情况下,这一个sigsuspend信号处理周期里面,将全部的子进程进行回收,那么master进程就可以立刻全身而退了,如图9所示:图9-理想退出情况当然,糟糕的情况总是会发生,这期间没有任何SIGCHLD信号产生,直到50ms到了产生SIGALRM信号,SIGALRM产生后,会将sigio重置为0,并将delay翻倍,设置一个新的定时器。当下个sigsuspend周期进来的时候,由于sigio为0,master进程会再次向worker进程广播NGX_CMD_TERMINATE消息(催促worker进程尽快退出)。如此往复,直到所有的子进程都退出,或者delay超过1000ms之后,master直接通过SIGKILL杀死子进程。图10-糟糕的退出场景timeout机制配置重新加载nginx支持在不停止服务的情况下,重新加载配置文件并生效。通过nginx -s reload即可。通过前面可以看到,nginx -s reload实际上是向master进程发送SIGHUP信号,信号处理器会置ngx_reconfigure为1。当主循环检测到ngx_reconfigure为1时,首先调用ngx_init_cycle函数构造一个新的生命周期cycle对象,重新加载配置文件。然后根据新的配置里设定的worker_processes启动新的worker进程。然后sleep 100ms来等待着子进程的启动和初始化,更新live为1,最后,通过socket channel向旧的worker进程发送NGX_CMD_QUIT消息,让旧的worker优雅退出。if (ngx_reconfigure) { ngx_reconfigure = 0; if (ngx_new_binary) { ngx_start_worker_processes(cycle, ccf->worker_processes, NGX_PROCESS_RESPAWN); ngx_start_cache_manager_processes(cycle, 0); ngx_noaccepting = 0; continue; } ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, “reconfiguring”); cycle = ngx_init_cycle(cycle); if (cycle == NULL) { cycle = (ngx_cycle_t *) ngx_cycle; continue; } ngx_cycle = cycle; ccf = (ngx_core_conf_t ) ngx_get_conf(cycle->conf_ctx, ngx_core_module); ngx_start_worker_processes(cycle, ccf->worker_processes, //fork新的worker进程 NGX_PROCESS_JUST_RESPAWN); ngx_start_cache_manager_processes(cycle, 1); / allow new processes to start */ ngx_msleep(100); live = 1; ngx_signal_worker_processes(cycle, //让旧的worker进程退出 ngx_signal_value(NGX_SHUTDOWN_SIGNAL));}可以看到,nginx并没有让旧的worker进程重新reload配置文件,而是通过新进程替换旧进程的方式来完成了配置文件的重新加载。对于master进程来说,如何区分新的worker进程和旧的worker进程呢?在fork新的worker时,传入的flag是NGX_PROCESS_JUST_RESPAWN,传入这个标记之后,fork的子进程的just_spawn和respawn2个标记会被置为1。而旧的worker在fork时传入的flag是NGX_PROCESS_RESPAWN,它只会将respawn标记置为1。因此,在通过socket channel发送NGX_CMD_QUIT命令时,如果发现子进程的just_spawn标记为1,那么就会忽略该命令(要不然新的worker进程也会被无辜杀死了),然后just_spwan标记会恢复为0(不然未来reload时,就无法区分新旧worker了)。细心的同学还可以看到,在上面还有一个当ngx_new_binary为真时的逻辑分支,它竟然直接使用旧的配置文件,fork新的子进程就continue了。对于这段代码我得理解是这样:ngx_new_binary上面提到过,是升级binary时的新master进程的pid,这个场景应该是正在升级binary过程中,旧的master进程还没有推出。如果这时通过nginx -s reload去重新加载配置文件,只会给新的master进程发送SIGHUP信号(因为这时的pid文件记录的新master进程的pid),因此走到这个逻辑分支,说明是手动使用kill -HUP发送给旧的master进程的,对于升级中这个中间过程,旧的master进程并没有重新加载最新的配置文件,因为没有必要,旧的master和旧worker进行最终的归宿是被杀死,所以这里就简单的fork了下,其实这里我觉得旧master进程忽略这个信号也未尝不可。重新打开文件在日志切分场景,重新打开文件这个feature非常有用。线上nginx服务产生的日志量是巨大的,随着时间的累积,会产生超大文件,对于排查问题非常不方便。所以日志切割很有必要,那么日志是如何切割的?直接mv nginx.log nginx.log.xxx,然后再新建一个nginx.log空文件,这样可行吗?答案当然是否。这涉及到fd,打开文件表和inode的概念。在这里简单描述下:见图11(引用网络图片),fd是进程级别的,fd会指向一个系统级的打开文件表中的一个表项。这个表项如果指代的是磁盘文件的话,会有一个指向磁盘inode节点的指针,并且这里还会存储文件偏移量等信息。磁盘文件是通过inode进行管理的,inode里会存储着文件的user、group、权限、时间戳、硬链接以及指向数据块的指针。进程通过fd写文件,最终写到的是inode节点对应的数据区域。如果我们通过mv命令对文件进行了重命名,实际上该fd与inode之间的映射链路并不会受到影响,也就是最终仍然向同一块数据区域写数据,最终表现就是,nginx.log.xxx中日志仍然会源源不断的产生。而新建的nginx.log空文件,它对应的是另外的inode节点,和fd毫无关系,因此,nginx.log不会有日志产生的。图11-fd、打开文件表、inode关系(引用网络图片)那么我们一般要怎么切割日志呢?实际上,上面的操作做对了一半,mv是没有问题的,接下来解决内存中fd映射到新的inode节点就可以搞定了。所以这就是重新打开文件发挥作用的时候了。向master进程发送SIGUSR1信号,在信号处理器里会置ngx_reopen全局标记为1。当主循环检测到ngx_reopen为1时,会调用ngx_reopen_files函数重新打开文件,生成新的fd,然后关闭旧的fd。然后通过socket channel向所有worker进程广播NGX_CMD_REOPEN指令,worker进程针对NGX_CMD_REOPEN指令也采取和master一样的动作。对于日志分割场景,重新打开之后的日志数据就可以在新的nginx.log中看到了,而nginx.log.xxx也不再会有数据写入,因为相应的fd都已close。升级binarynginx支持不停止服务的情况下,平滑升级nginx binary程序。一般的操作步骤是: - 1、先向master进程发送SIGUSR2信号,产生新的master和新的worker进程。(注意这时同时存在2个master+worker集群) - 2、向旧的master进程发送SIGWINCH信号,这样旧的worker进程就会全部退出。 - 3、新的集群如果服务正常的话,就可以向旧的master进程发送SIGQUIT信号,让它退出。master进程收到SIGUSR2信号后,信号处理器会置ngx_change_binary为1。主循环检测到该标记位后,会调用ngx_exec_new_binary函数产生一个新的master进程,并且将新master进程的pid赋值给ngx_new_binary。让我们看下ngx_exec_new_binary如何产生新master进程的。首先会构建一个ngx_exec_ctx_t类型的临时变量ctx,ngx_exec_ctx_t结构体如下:typedef struct {char *path; //binary路径char *name; //新进程名称char *const *argv; //参数char *const *envp; //环境变量} ngx_exec_ctx_t;如图12所示,所示将ctx.path置为启动master进程的nginx程序路径,比如"/home/xiaoju/nginx-jiweibin/sbin/nginx",ctx.name置为"new binary process",ctx.argv置为nginx main函数执行时传入的参数集合。对于环境变量,除了继承当前master进程的环境变量外,会构造一个名为NGINX的环境变量,它的取值是所有监听的socket对应fd按";“分割,例如:NGINX=“8;9;10;…"。这个环境变量很关键,下面会提到它的作用。图12-ngx_exec_ctx_t ctx示意图构造完ctx后,将pid文件重命名,后面加上”.old"后缀。然后调用ngx_execute函数。这个函数内部会通过ngx_spawn_process函数fork一个新的子进程,该进程的标记detached为1,表示是游离进程。该子进程一旦启动后,会执行ngx_execute_proc函数,这里会执行execve系统调用,重新执行ctx.path,即exec nginx程序。这样,新的master进程就通过fork+execve2个系统调用启动起来了。随后,新master进程会启动新的的worker进程。ngx_pid_tngx_execute(ngx_cycle_t *cycle, ngx_exec_ctx_t *ctx){ return ngx_spawn_process(cycle, ngx_execute_proc, ctx, ctx->name, //fork 新的子进程 NGX_PROCESS_DETACHED);} static voidngx_execute_proc(ngx_cycle_t *cycle, void *data) //fork新的mast{ ngx_exec_ctx_t *ctx = data; if (execve(ctx->path, ctx->argv, ctx->envp) == -1) { ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno, “execve() failed while executing %s "%s"”, ctx->name, ctx->path); } exit(1);}其实这里是有一个问题要解决的:旧的master进程对于80,8080这种监听端口已经bind并且listen了,如果新的master进程进行同样的bind操作,会产生类似这种错误:nginx: [emerg] bind() to 0.0.0.0:8080 failed (98: Address already in use)。所以,master进程是如何做到监听这些端口的呢? 让我们先了解exec(execve是exec系列系统调用的一种)这个系统调用,它并不改变进程的pid,但是它会用新的程序(这里还是nginx)替换现有进程的代码段,数据段,BSS,堆,栈。比如ngx_processes这个全局变量,它处于BSS段,在exec之后,这个数据会清空,新的master不会通过ngx_processes数组引用到旧的worker进程。同理,存储着所有监听的数据结构cycle.listening由于在进程的堆上,同样也会清空。但fd比较特殊,对于进程创建的fd,exec之后仍然有效(除非设置了FD_CLOEXEC标记,nginx的打开的相关文件都设置了这个标记,但监听socket对应的fd没有设置)。所以旧的master打开了某一个80端口的fd假设是9,那么在新的master进程,仍然可以继续使用这个fd。所以问题就变成了,如何让新的master进程知道这些fd的存在,并重新构建cycle.listening数组?这就用到了上面提到的NGINX这个环境变量,它将所有的fd通过NGINX传递给新master进程,新master进程看到这个环境变量后,就可以根据它的值,重新构建cycle.listening数组啦。代码如下:static ngx_int_tngx_add_inherited_sockets(ngx_cycle_t *cycle){ u_char *p, *v, *inherited; ngx_int_t s; ngx_listening_t *ls; inherited = (u_char *) getenv(NGINX_VAR); if (inherited == NULL) { return NGX_OK; } ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, “using inherited sockets from "%s"”, inherited); if (ngx_array_init(&cycle->listening, cycle->pool, 10, sizeof(ngx_listening_t)) != NGX_OK) { return NGX_ERROR; } for (p = inherited, v = p; *p; p++) { if (*p == ‘:’ || *p == ‘;’) { s = ngx_atoi(v, p - v); if (s == NGX_ERROR) { ngx_log_error(NGX_LOG_EMERG, cycle->log, 0, “invalid socket number "%s" in " NGINX_VAR " environment variable, ignoring the rest” " of the variable”, v); break; } v = p + 1; ls = ngx_array_push(&cycle->listening); if (ls == NULL) { return NGX_ERROR; } ngx_memzero(ls, sizeof(ngx_listening_t)); ls->fd = (ngx_socket_t) s; } } ngx_inherited = 1; return ngx_set_inherited_sockets(cycle);}这里还有一个需要知道的细节,旧master进程fork子进程并exec nginx程序之后,并不会像上面的daemon模式一样,再fork一个子进程作为master,因为这个子进程不属于任何终端,不会随着终端退出而退出,因此这个exec之后的子进程就是新master进程,那么nginx程序是如何区分这2种启动模式的呢?同样也是基于NGINX这个环境变量,如上面代码所示,如果存在这个环境变量,ngx_inherited会被置为1,当nginx检测到这个标记位为1时,就不会再fork子进程作为master了,而是本身就是master进程。当旧的master进程收到SIGWINCH信号,信号处理器会置ngx_noaccept为1。当主循环检测到这个标记时,会置ngx_noaccepting为1,表示旧的master进程下的worker进程陆续都会退出,不再对外服务了。然后通过socket channel通知所有的worker进程NGX_CMD_QUIT指令,worker进程收到该指令,会优雅的退出(注意,这里的worker进程是指旧master进程管理的worker进程,为什么通知不到新的worker进程,大家可以想下为什么)。最后,当新的worker进程服务正常之后,可以放心的杀死旧的master进程了。为什么不通过SIGQUIT一步杀死旧的master+worker呢?之所以不这么做,是为了可以随时回滚。当我们发现新的binary有问题时,如果旧的master进程被我干掉了,我们还要使用backup的旧的binary再启动,这个切换时间一旦过长,会造成比较严重的影响,可能更糟糕的情况是你根本没有对旧的binary进程备份,这样就需要回滚代码,重新编译,安装。整个回滚的时间会更加不可控。所以,当我们再升级binary时,一般都要留着旧master进程,因为它可以按照旧的binary随时重启worker进程。还记得上面讲到子进程退出的逻辑吗,新的master进程是旧master进程的child,当新master进程退出,并且ngx_noaccepting为1,即旧master进程已经杀了了它的worker(不包括新master,因为它是detached),那么会置ngx_restart为1,当主循环检测到这个全局标记位,会再次启动worker进程,让旧的binary恢复工作。if (ngx_restart) { ngx_restart = 0; ngx_start_worker_processes(cycle, ccf->worker_processes, NGX_PROCESS_RESPAWN); ngx_start_cache_manager_processes(cycle, 0); live = 1;}七、总结本篇wiki分析了master进程启动,基于信号的事件循环架构,基于各种标记位的相应进程的管理,包括进程退出,配置文件变更,重新打开文件,升级binary以及master和worker通信的一种方式之一:socket channel。希望大家有所收获。 ...

October 13, 2018 · 4 min · jiezi

vue路由history模式刷新页面出现404问题

vue hash模式下,URL中存在’#’,用’history’模式就能解决这个问题。但是history模式会出现刷新页面后,页面出现404。解决的办法是用nginx配置一下。在nginx的配置文件中修改方法一:location /{ root /data/nginx/html; index index.html index.htm; if (!-e $request_filename) { rewrite ^/(.) /index.html last; break; }}方法二:vue.js官方教程里提到的https://router.vuejs.org/zh/g… server { listen 8081;#默认端口是80,如果端口没被占用可以不用修改 server_name myapp.com; root D:/vue/my_app/dist;#vue项目的打包后的dist location / { try_files $uri $uri/ @router;#需要指向下面的@router否则会出现vue的路由在nginx中刷新出现404 index index.html index.htm; } #对应上面的@router,主要原因是路由的路径资源并不是一个真实的路径,所以无法找到具体的文件 #因此需要rewrite到index.html中,然后交给路由在处理请求资源 location @router { rewrite ^.$ /index.html last; } #…….其他部分省略 }

October 12, 2018 · 1 min · jiezi

关于MySQL 通用查询日志和慢查询日志分析

MySQL中的日志包括:错误日志、二进制日志、通用查询日志、慢查询日志等等。这里主要介绍下比较常用的两个功能:通用查询日志和慢查询日志。1)通用查询日志:记录建立的客户端连接和执行的语句。2)慢查询日志:记录所有执行时间超过longquerytime秒的所有查询或者不使用索引的查询(1)通用查询日志在学习通用日志查询时,需要知道两个数据库中的常用命令:1) show variables like ‘%general%’;可以查看,当前的通用日志查询是否开启,如果general_log的值为ON则为开启,为OFF则为关闭(默认情况下是关闭的)。1) show variables like ‘%log_output%’;查看当前慢查询日志输出的格式,可以是FILE(存储在数数据库的数据文件中的hostname.log),也可以是TABLE(存储在数据库中的mysql.general_log)问题:如何开启MySQL通用查询日志,以及如何设置要输出的通用日志输出格式呢?开启通用日志查询: set global general_log=on;关闭通用日志查询: set global general_log=off;设置通用日志输出为表方式: set global log_output=’TABLE’;设置通用日志输出为文件方式: set global log_output=’FILE’;设置通用日志输出为表和文件方式:set global log_output=’FILE,TABLE’;(注意:上述命令只对当前生效,当MySQL重启失效,如果要永久生效,需要配置 my.cnf)my.cnf文件的配置如下:general_log=1 #为1表示开启通用日志查询,值为0表示关闭通用日志查询log_output=FILE,TABLE#设置通用日志的输出格式为文件和表(2)慢查询日志MySQL的慢查询日志是MySQL提供的一种日志记录,用来记录在MySQL中响应时间超过阈值的语句,具体指运行时间超过long_query_time值的SQL,则会被记录到慢查询日志中(日志可以写入文件或者数据库表,如果对性能要求高的话,建议写文件)。默认情况下,MySQL数据库是不开启慢查询日志的,long_query_time的默认值为10(即10秒,通常设置为1秒),即运行10秒以上的语句是慢查询语句。一般来说,慢查询发生在大表(比如:一个表的数据量有几百万),且查询条件的字段没有建立索引,此时,要匹配查询条件的字段会进行全表扫描,耗时查过long_query_time,则为慢查询语句。问题:如何查看当前慢查询日志的开启情况?在MySQL中输入命令:show variables like ‘%quer%’;主要掌握以下的几个参数:(1)slow_query_log的值为ON为开启慢查询日志,OFF则为关闭慢查询日志。(2)slow_query_log_file 的值是记录的慢查询日志到文件中(注意:默认名为主机名.log,慢查询日志是否写入指定文件中,需要指定慢查询的输出日志格式为文件,相关命令为:show variables like ‘%log_output%’;去查看输出的格式)。(3)long_query_time 指定了慢查询的阈值,即如果执行语句的时间超过该阈值则为慢查询语句,默认值为10秒。(4)log_queries_not_using_indexes 如果值设置为ON,则会记录所有没有利用索引的查询(注意:如果只是将log_queries_not_using_indexes设置为ON,而将slow_query_log设置为OFF,此时该设置也不会生效,即该设置生效的前提是slow_query_log的值设置为ON),一般在性能调优的时候会暂时开启。问题:设置MySQL慢查询的输出日志格式为文件还是表,或者两者都有?通过命令:show variables like ‘%log_output%’;通过log_output的值可以查看到输出的格式,上面的值为TABLE。当然,我们也可以设置输出的格式为文本,或者同时记录文本和数据库表中,设置的命令如下:慢查询日志输出到表中(即mysql.slow_log)set globallog_output=’TABLE’;慢查询日志仅输出到文本中(即:slow_query_log_file指定的文件)setglobal log_output=’FILE’;在此我向大家推荐一个架构学习交流群。交流学习群号:478030634 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多慢查询日志同时输出到文本和表中setglobal log_output=’FILE,TABLE’; 关于慢查询日志的表中的数据个文本中的数据格式分析:慢查询的日志记录myql.slow_log表中,格式如下:慢查询的日志记录到hostname.log文件中,格式如下:可以看到,不管是表还是文件,都具体记录了:是那条语句导致慢查询(sql_text),该慢查询语句的查询时间(query_time),锁表时间(Lock_time),以及扫描过的行数(rows_examined)等信息。问题:如何查询当前慢查询的语句的个数?在MySQL中有一个变量专门记录当前慢查询语句的个数:输入命令:show global status like ‘%slow%’;(注意:上述所有命令,如果都是通过MySQL的shell将参数设置进去,如果重启MySQL,所有设置好的参数将失效,如果想要永久的生效,需要将配置参数写入my.cnf文件中)。补充知识点:如何利用MySQL自带的慢查询日志分析工具mysqldumpslow分析日志?perlmysqldumpslow –s c –t 10 slow-query.log具体参数设置如下:-s 表示按何种方式排序,c、t、l、r分别是按照记录次数、时间、查询时间、返回的记录数来排序,ac、at、al、ar,表示相应的倒叙;-t 表示top的意思,后面跟着的数据表示返回前面多少条;-g 后面可以写正则表达式匹配,大小写不敏感。上述中的参数含义如下:Count:414 语句出现了414次;Time=3.51s(1454) 执行最长时间为3.51s,累计总耗费时间1454s;Lock=0.0s(0) 等待锁最长时间为0s,累计等待锁耗费时间为0s;Rows=2194.9(9097604) 发送给客户端最多的行数为2194.9,累计发送给客户端的函数为90976404(注意:mysqldumpslow脚本是用perl语言写的,具体mysqldumpslow的用法后期再讲)问题:实际在学习过程中,如何得知设置的慢查询是有效的?很简单,我们可以手动产生一条慢查询语句,比如,如果我们的慢查询log_query_time的值设置为1,则我们可以执行如下语句:selectsleep(1);该条语句即是慢查询语句,之后,便可以在相应的日志输出文件或表中去查看是否有该条语句。大家觉得文章对你还是有一点点帮助的,大家可以点击下方二维码进行关注。 《乐趣区》 公众号聊的不仅仅是Java技术知识,还有面试等干货,后期还有大量架构干货。大家一起关注吧!关注烂猪皮,你会了解的更多…………..原文:https://my.oschina.net/u/3575…

October 11, 2018 · 1 min · jiezi

深入理解高并发下分布式事务的解决方案

1、什么是分布式事务分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。以上是百度百科的解释,简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。2、分布式事务的产生的原因2.1、数据库分库分表当数据库单表一年产生的数据超过1000W,那么就要考虑分库分表,具体分库分表的原理在此不做解释,以后有空详细说,简单的说就是原来的一个数据库变成了多个数据库。这时候,如果一个操作既访问01库,又访问02库,而且要保证数据的一致性,那么就要用到分布式事务。2.2、应用SOA化所谓的SOA化,就是业务的服务化。比如原来单机支撑了整个电商网站,现在对整个网站进行拆解,分离出了订单中心、用户中心、库存中心。对于订单中心,有专门的数据库存储订单信息,用户中心也有专门的数据库存储用户信息,库存中心也会有专门的数据库存储库存信息。这时候如果要同时对订单和库存进行操作,那么就会涉及到订单数据库和库存数据库,为了保证数据一致性,就需要用到分布式事务。以上两种情况表象不同,但是本质相同,都是因为要操作的数据库变多了!3、事务的ACID特性3.1、原子性(A)所谓的原子性就是说,在整个事务中的所有操作,要么全部完成,要么全部不做,没有中间状态。对于事务在执行中发生错误,所有的操作都会被回滚,整个事务就像从没被执行过一样。3.2、一致性(C)事务的执行必须保证系统的一致性,就拿转账为例,A有500元,B有300元,如果在一个事务里A成功转给B50元,那么不管并发多少,不管发生什么,只要事务执行成功了,那么最后A账户一定是450元,B账户一定是350元。3.3、隔离性(I)所谓的隔离性就是说,事务与事务之间不会互相影响,一个事务的中间状态不会被其他事务感知。3.4、持久性(D)所谓的持久性,就是说一单事务完成了,那么事务对数据所做的变更就完全保存在了数据库中,即使发生停电,系统宕机也是如此。4、分布式事务的应用场景4.1、支付最经典的场景就是支付了,一笔支付,是对买家账户进行扣款,同时对卖家账户进行加钱,这些操作必须在一个事务里执行,要么全部成功,要么全部失败。而对于买家账户属于买家中心,对应的是买家数据库,而卖家账户属于卖家中心,对应的是卖家数据库,对不同数据库的操作必然需要引入分布式事务。4.2、在线下单买家在电商平台下单,往往会涉及到两个动作,一个是扣库存,第二个是更新订单状态,库存和订单一般属于不同的数据库,需要使用分布式事务保证数据一致性。5、常见的分布式事务解决方案5.1、基于XA协议的两阶段提交XA是一个分布式事务协议,由Tuxedo提出。XA中大致分为两部分:事务管理器和本地资源管理器。其中本地资源管理器往往由数据库实现,比如Oracle、DB2这些商业数据库都实现了XA接口,而事务管理器作为全局的调度者,负责各个本地资源的提交和回滚。XA实现分布式事务的原理如下:总的来说,XA协议比较简单,而且一旦商业数据库实现了XA协议,使用分布式事务的成本也比较低。但是,XA也有致命的缺点,那就是性能不理想,特别是在交易下单链路,往往并发量很高,XA无法满足高并发场景。XA目前在商业数据库支持的比较理想,在mysql数据库中支持的不太理想,mysql的XA实现,没有记录prepare阶段日志,主备切换回导致主库与备库数据不一致。许多nosql也没有支持XA,这让XA的应用场景变得非常狭隘。5.2、消息事务+最终一致性所谓的消息事务就是基于消息中间件的两阶段提交,本质上是对消息中间件的一种特殊利用,它是将本地事务和发消息放在了一个分布式事务里,保证要么本地操作成功成功并且对外发消息成功,要么两者都失败,开源的RocketMQ就支持这一特性,具体原理如下:1、A系统向消息中间件发送一条预备消息2、消息中间件保存预备消息并返回成功3、A执行本地事务4、A发送提交消息给消息中间件通过以上4步完成了一个消息事务。对于以上的4个步骤,每个步骤都可能产生错误,下面一一分析:步骤一出错,则整个事务失败,不会执行A的本地操作步骤二出错,则整个事务失败,不会执行A的本地操作步骤三出错,这时候需要回滚预备消息,怎么回滚?答案是A系统实现一个消息中间件的回调接口,消息中间件会去不断执行回调接口,检查A事务执行是否执行成功,如果失败则回滚预备消息步骤四出错,这时候A的本地事务是成功的,那么消息中间件要回滚A吗?答案是不需要,其实通过回调接口,消息中间件能够检查到A执行成功了,这时候其实不需要A发提交消息了,消息中间件可以自己对消息进行提交,从而完成整个消息事务基于消息中间件的两阶段提交往往用在高并发场景下,将一个分布式事务拆成一个消息事务(A系统的本地操作+发消息)+B系统的本地操作,其中B系统的操作由消息驱动,只要消息事务成功,那么A操作一定成功,消息也一定发出来了,这时候B会收到消息去执行本地操作,如果本地操作失败,消息会重投,直到B操作成功,这样就变相地实现了A与B的分布式事务。原理如下:虽然上面的方案能够完成A和B的操作,但是A和B并不是严格一致的,而是最终一致的,我们在这里牺牲了一致性,换来了性能的大幅度提升。当然,这种玩法也是有风险的,如果B一直执行不成功,那么一致性会被破坏,具体要不要玩,还是得看业务能够承担多少风险。5.3、TCC编程模式所谓的TCC编程模式,也是两阶段提交的一个变种。TCC提供了一个编程框架,将整个业务逻辑分为三块:Try、Confirm和Cancel三个操作。以在线下单为例,Try阶段会去扣库存,Confirm阶段则是去更新订单状态,如果更新订单失败,则进入Cancel阶段,会去恢复库存。总之,TCC就是通过代码人为实现了两阶段提交,不同的业务场景所写的代码都不一样,复杂度也不一样,因此,这种模式并不能很好地被复用。在此我向大家推荐一个架构学习交流群。交流学习群号:478030634 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多6、总结分布式事务,本质上是对多个数据库的事务进行统一控制,按照控制力度可以分为:不控制、部分控制和完全控制。不控制就是不引入分布式事务,部分控制就是各种变种的两阶段提交,包括上面提到的消息事务+最终一致性、TCC模式,而完全控制就是完全实现两阶段提交。部分控制的好处是并发量和性能很好,缺点是数据一致性减弱了,完全控制则是牺牲了性能,保障了一致性,具体用哪种方式,最终还是取决于业务场景。作为技术人员,一定不能忘了技术是为业务服务的,不要为了技术而技术,针对不同业务进行技术选型也是一种很重要的能力

October 10, 2018 · 1 min · jiezi

深入理解高并发下分布式事务的方案

编辑推荐:本文主要从分布式的原因,事务特性,和解决方案中深入理解了分布式事务,希望对您的学习有所帮助。1、什么是分布式事务分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。以上是百度百科的解释,简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。2、分布式事务的产生的原因2.1、数据库分库分表当数据库单表一年产生的数据超过1000W,那么就要考虑分库分表,具体分库分表的原理在此不做解释,以后有空详细说,简单的说就是原来的一个数据库变成了多个数据库。这时候,如果一个操作既访问01库,又访问02库,而且要保证数据的一致性,那么就要用到分布式事务。2.2、应用SOA化所谓的SOA化,就是业务的服务化。比如原来单机支撑了整个电商网站,现在对整个网站进行拆解,分离出了订单中心、用户中心、库存中心。对于订单中心,有专门的数据库存储订单信息,用户中心也有专门的数据库存储用户信息,库存中心也会有专门的数据库存储库存信息。这时候如果要同时对订单和库存进行操作,那么就会涉及到订单数据库和库存数据库,为了保证数据一致性,就需要用到分布式事务。以上两种情况表象不同,但是本质相同,都是因为要操作的数据库变多了!3、事务的ACID特性3.1、原子性(A)所谓的原子性就是说,在整个事务中的所有操作,要么全部完成,要么全部不做,没有中间状态。对于事务在执行中发生错误,所有的操作都会被回滚,整个事务就像从没被执行过一样。3.2、一致性(C)事务的执行必须保证系统的一致性,就拿转账为例,A有500元,B有300元,如果在一个事务里A成功转给B50元,那么不管并发多少,不管发生什么,只要事务执行成功了,那么最后A账户一定是450元,B账户一定是350元。3.3、隔离性(I)所谓的隔离性就是说,事务与事务之间不会互相影响,一个事务的中间状态不会被其他事务感知。3.4、持久性(D)所谓的持久性,就是说一单事务完成了,那么事务对数据所做的变更就完全保存在了数据库中,即使发生停电,系统宕机也是如此。4、分布式事务的应用场景4.1、支付最经典的场景就是支付了,一笔支付,是对买家账户进行扣款,同时对卖家账户进行加钱,这些操作必须在一个事务里执行,要么全部成功,要么全部失败。而对于买家账户属于买家中心,对应的是买家数据库,而卖家账户属于卖家中心,对应的是卖家数据库,对不同数据库的操作必然需要引入分布式事务。4.2、在线下单买家在电商平台下单,往往会涉及到两个动作,一个是扣库存,第二个是更新订单状态,库存和订单一般属于不同的数据库,需要使用分布式事务保证数据一致性。5、常见的分布式事务解决方案5.1、基于XA协议的两阶段提交XA是一个分布式事务协议,由Tuxedo提出。XA中大致分为两部分:事务管理器和本地资源管理器。其中本地资源管理器往往由数据库实现,比如Oracle、DB2这些商业数据库都实现了XA接口,而事务管理器作为全局的调度者,负责各个本地资源的提交和回滚。XA实现分布式事务的原理如下:总的来说,XA协议比较简单,而且一旦商业数据库实现了XA协议,使用分布式事务的成本也比较低。但是,XA也有致命的缺点,那就是性能不理想,特别是在交易下单链路,往往并发量很高,XA无法满足高并发场景。XA目前在商业数据库支持的比较理想,在mysql数据库中支持的不太理想,mysql的XA实现,没有记录prepare阶段日志,主备切换回导致主库与备库数据不一致。许多nosql也没有支持XA,这让XA的应用场景变得非常狭隘。5.2、消息事务+最终一致性所谓的消息事务就是基于消息中间件的两阶段提交,本质上是对消息中间件的一种特殊利用,它是将本地事务和发消息放在了一个分布式事务里,保证要么本地操作成功成功并且对外发消息成功,要么两者都失败,开源的RocketMQ就支持这一特性,具体原理如下:1、A系统向消息中间件发送一条预备消息2、消息中间件保存预备消息并返回成功3、A执行本地事务4、A发送提交消息给消息中间件通过以上4步完成了一个消息事务。对于以上的4个步骤,每个步骤都可能产生错误,下面一一分析:步骤一出错,则整个事务失败,不会执行A的本地操作步骤二出错,则整个事务失败,不会执行A的本地操作步骤三出错,这时候需要回滚预备消息,怎么回滚?答案是A系统实现一个消息中间件的回调接口,消息中间件会去不断执行回调接口,检查A事务执行是否执行成功,如果失败则回滚预备消息步骤四出错,这时候A的本地事务是成功的,那么消息中间件要回滚A吗?答案是不需要,其实通过回调接口,消息中间件能够检查到A执行成功了,这时候其实不需要A发提交消息了,消息中间件可以自己对消息进行提交,从而完成整个消息事务基于消息中间件的两阶段提交往往用在高并发场景下,将一个分布式事务拆成一个消息事务(A系统的本地操作+发消息)+B系统的本地操作,其中B系统的操作由消息驱动,只要消息事务成功,那么A操作一定成功,消息也一定发出来了,这时候B会收到消息去执行本地操作,如果本地操作失败,消息会重投,直到B操作成功,这样就变相地实现了A与B的分布式事务。原理如下:虽然上面的方案能够完成A和B的操作,但是A和B并不是严格一致的,而是最终一致的,我们在这里牺牲了一致性,换来了性能的大幅度提升。当然,这种玩法也是有风险的,如果B一直执行不成功,那么一致性会被破坏,具体要不要玩,还是得看业务能够承担多少风险。5.3、TCC编程模式所谓的TCC编程模式,也是两阶段提交的一个变种。TCC提供了一个编程框架,将整个业务逻辑分为三块:Try、Confirm和Cancel三个操作。以在线下单为例,Try阶段会去扣库存,Confirm阶段则是去更新订单状态,如果更新订单失败,则进入Cancel阶段,会去恢复库存。总之,TCC就是通过代码人为实现了两阶段提交,不同的业务场景所写的代码都不一样,复杂度也不一样,因此,这种模式并不能很好地被复用。6、总结分布式事务,本质上是对多个数据库的事务进行统一控制,按照控制力度可以分为:不控制、部分控制和完全控制。不控制就是不引入分布式事务,部分控制就是各种变种的两阶段提交,包括上面提到的消息事务+最终一致性、TCC模式,而完全控制就是完全实现两阶段提交。部分控制的好处是并发量和性能很好,缺点是数据一致性减弱了,完全控制则是牺牲了性能,保障了一致性,具体用哪种方式,最终还是取决于业务场景。作为技术人员,一定不能忘了技术是为业务服务的,不要为了技术而技术,针对不同业务进行技术选型也是一种很重要的能力。顺便在此给大家推荐一个Java架构方面的交流学习群:698581634,里面会分享一些资深架构师录制的视频资料:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系,主要针对Java开发人员提升自己,突破瓶颈,相信你来学习,会有提升和收获。在这个群里会有你需要的内容 朋友们请抓紧时间加入进来吧。

October 10, 2018 · 1 min · jiezi

百亿级日志系统架构设计及优化'

本文将从海量日志系统在优化、部署、监控方向如何更适应业务的需求入手,重点从多种日志系统的架构设计对比;后续调优过程:横向扩展与纵向扩展,分集群,数据分治,重写数据链路等实际现象与问题展开。日志系统架构基准有过项目开发经验的朋友都知道:从平台的最初搭建到实现核心业务,都需要有日志平台为各种业务保驾护航。如上图所示,对于一个简单的日志应用场景,通常会准备 master/slave 两个应用。我们只需运行一个 Shell 脚本,便可查看是否存在错误信息。随着业务复杂度的增加,应用场景也会变得复杂。虽然监控系统能够显示某台机器或者某个应用的错误。然而在实际的生产环境中,由于实施了隔离,一旦在上图下侧的红框内某个应用出现了 Bug,则无法访问到其对应的日志,也就谈不上将日志取出了。另外,有些深度依赖日志平台的应用,也可能在日志产生的时候就直接采集走,进而删除掉原始的日志文件。这些场景给我们日志系统的维护都带来了难度。参考 Logstash,一般会有两种日志业务流程:正常情况下的简单流程为:应用产生日志→根据预定义的日志文件大小或时间间隔,通过执行 Logrotation,不断刷新出新的文件→定期查看→定期删除。复杂应用场景的流程为:应用产生日志→采集→传输→按需过滤与转换→存储→分析与查看。我们可以从实时性和错误分析两个维度来区分不同的日志数据场景:实时,一般适用于我们常说的一级应用,如:直接面向用户的应用。我们可以自定义各类关键字,以方便在出现各种 error 或 exception 时,相关业务人员能够在第一时间被通知到。准实时,一般适用于一些项目管理的平台,如:在需要填写工时的时候出现了宕机,但这并不影响工资的发放。平台在几分钟后完成重启,我们可以再登录填写,该情况并不造成原则性的影响。因此,我们可以将其列为准实时的级别。除了直接采集错误与异常,我们还需要进行分析。例如:仅知道某人的体重是没什么意义的,但是如果增加了性别和身高两个指标,那么我们就可以判断出此人的体重是否为标准体重。也就是说:如果能给出多个指标,就可以对庞大的数据进行去噪,然后通过回归分析,让采集到的数据更有意义。此外,我们还要不断地去还原数字的真实性。特别是对于实时的一级应用,我们要能快速地让用户明白他们所碰到现象的真实含义。例如:商家在上架时错把商品的价格标签 100 元标成了 10 元。这会导致商品马上被抢购一空。但是这种现象并非是业务的问题,很难被发现,因此我们只能通过日志数据进行逻辑分析,及时反馈以保证在几十秒之后将库存修改为零,从而有效地解决此问题。可见,在此应用场景中,实时分析就显得非常有用。最后是追溯,我们需要在获取历史信息的同时,实现跨时间维度的对比与总结,那么追溯就能够在各种应用中发挥其关联性作用了。上述提及的各个要素都是我们管理日志的基准。如上图所示,我们的日志系统采用的是开源的 ELK 模式:ElasticSearch(后简称 ES),负责后端集中存储与查询工作。单独的 Beats 负责日志的搜集。FileBeat 则改进了 Logstash 的资源占用问题;TopBeat 负责搜集监控资源,类似系统命令 top 去获取 CPU 的性能。由于日志服务对于业务来说仅起到了维稳和保障的作用,而且我们需要实现快速、轻量的数据采集与传输,因此不应占用服务器太多资源。在方式上我们采用的是插件模式,包括:input 插件、output 插件、以及中间负责传输过滤的插件。这些插件有着不同的规则和自己的格式,支持着各种安全性的传输。日志系统优化思路有了上述日志的架构,我们针对各种实际的应用场景,进一步提出了四个方面的优化思路:基础优化内存:如何分配内存、垃圾回收、增加缓存和锁。网络:网络传输序列化、增加压缩、策略、散列、不同协议与格式。CPU:用多线程提高利用率和负载。此处利用率和负载是两个不同的概念:利用率:在用满一个核后再用下一个内核,利用率是逐步升高的。负载:一下子把八个核全用上了,则负载虽然是满的,但是利用率很低。即,每核都被占用了,但是所占用的资源却不多,计算率比较低下。磁盘:尝试通过文件合并,减少碎片文件的产生,并减少寻道次数。同时在系统级别,通过修改设置,关闭各种无用的服务。平台扩展做加减法,或称替代方案:无论是互联网应用,还是日常应用,我们在查询时都增加了分布式缓存,以有效提升查询的效率。另外,我们将不被平台使用到的地方直接关闭或去除。纵向扩展:如增加扩展磁盘和内存。横向扩展:加减/平行扩展,使用分布式集群。数据分治根据数据的不同维度,对数据进行分类、分级。例如:我们从日志中区分error、info、和 debug,甚至将 info 和 debug 级别的日志直接过滤掉。数据热点:例如:某种日志数据在白天的某个时间段内呈现暴涨趋势,而晚上只是平稳产生。我们就可以根据此热点情况将它们取出来单独处理,以打散热点。系统降级我们在对整体业务进行有效区分的基础上,通过制定一些降级方案,将部分不重要的功能停掉,以满足核心业务。在此我向大家推荐一个架构学习交流群。交流学习群号:478030634 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多日志系统优化实践面对持续增长的数据量,我们虽然增加了许多资源,但是并不能从根本上解决问题。特别体现在如下三方面:日志产生量庞大,每天有几百亿条。由于生产环境隔离,我们无法直接查看到数据。代理资源限制,我们的各种日志采集和系统资源采集操作,不可超过业务资源的一个核。一级业务架构我们日志系统的层次相对比较清晰,可简单分为数据接入、数据存储和数据可视化三大块。具体包括:Rsyslog,是目前我们所接触到的采集工具中最节省性能的一种。Kafka,具有持久化的作用。当然它在使用到达一定数据量级时,会出现 Bug。Fluentd,它与 Rsyslog 类似,也是一种日志的传输工具,但是它更偏向传输服务。ES 和 Kibana。该架构在实现上会用到 Golang、Ruby、Java、JS 等不同的语言。在后期改造时,我们会将符合 Key-Value 模式的数据快速地导入 HBase 之中。基于 HBase 的自身特点,我们实现了它在内存层的 B+ 树,并且持久化到我们的磁盘之上,从而达到了理想的快速插入的速度。这也正是我们愿意选择 HBase 作为日志方案的原因。二级业务架构我们直接来看二级业务架构的功能图,它是由如下流程串联而成的:在完成了数据采集之后,为了节省自己占用磁盘的空间,许多应用会完全依赖于我们的日志系统。因此在数据采集完以后,我们增加了一个持久缓存。完成缓存之后系统执行传输。传输的过程包括:过滤和转换,这个过程可以进行数据抽稀。值得强调的是:如果业务方尽早合作并给予我们一些约定的话,我们就能够通过格式化来实现结构化的数据。随后执行的是分流,其主要包括两大块:一种是 A 来源的数据走 A 通道,B 来源的数据走 B 通道。另一种是让 A 数据流入到我们的存储设备,并触发保护机制。即为了保障存储系统,我们额外增加了一个队列。例如:队列为 100,里面的一个 chunk 为 256 兆,我们现在设置高水位为 0.7、低水位为 0.3。在写操作的堆积时,由于我们设置了 0.7,即 100 兆赫。那么在一个 256 兆会堆积到 70 个 chunk 时,我们往该存储平台的写速度就已经跟不上了。此时高水位点会被触发,不允许继续写入,直到整个写入过程把该 chunk 消化掉,并降至 30 个时,方可继续往里写入。我们就是用该保护机制来保护后台以及存储设备的。接着是存储,由于整个数据流的量会比较大,因此在存储环节主要执行的是存储的索引、压缩、和查询。最后是 UI 的一些分析算法,运用 SQL 的一些查询语句进行简单、快速地查询。通常从采集(logstash/rsyslog/heka/filebeat)到面向缓存的 Kafka 是一种典型的宽依赖。所谓宽依赖,是指每个 App 都可能跟每个 Broker 相关联。在 Kafka 处,每次传输都要在哈希之后,再把数据写到每个 Broker 上。而窄依赖,则是其每一个 Fluentd 进程都只对应一个 Broker 的过程。最终通过宽依赖过程写入到 ES。采集如 Rsyslog 不但占用资源最少,而且可以添加各种规则,它还能支持像 TSL、SSL 之类的安全协议。Filebeat 轻量,在版本 5.x 中,Elasticsearch 具有解析的能力(像 Logstash 过滤器)— Ingest。这也就意味着可以将数据直接用 Filebeat 推送到 Elasticsearch,并让 Elasticsearch 既做解析的事情,又做存储的事情。Kafka接着是 Kafka,Kafka 主要实现的是顺序存储,它通过 topic 和消息队列的机制,实现了快速地数据存储。而它的缺点:由于所有的数据都向 Kafka 写入,会导致 topic 过多,引发磁盘竞争,进而严重拖累 Kafka 的性能。另外,如果所有的数据都使用统一标签的话,由于不知道所采集到的数据具体类别,我们将很难实现对数据的分治。因此,在后面的优化传输机制方面,我们改造并自己实现了顺序存储的过程,进而解决了一定要做持久化这一安全保障的需求。FluentdFluentd 有点类似于 Logstash,它的文档和插件非常齐全。其多种插件可保证直接对接到 Hadoop 或 ES。就接入而言,我们可以采用 Fluentd 到 Fluentd 的方式。即在原有一层数据接入的基础上,再接一次 Fluentd。同时它也支持安全传输。当然我们在后面也对它进行了重点优化。ES+Kibana最后我们用到了 ES 和 Kibana。ES 的优势在于通过 Lucene 实现了快速的倒排索引。由于大量的日志是非结构化的,因此我们使用 ES 的 Lucene 进行包装,以满足普通用户执行非结构化日志的搜索。而 Kibana 则基于 Lucene 提供可视化显示工具。问题定位与解决下面介绍一下我们碰到过的问题和现象,如下这些都是我们着手优化的出发点:传输服务器的 CPU 利用率低下,每个核的负载不饱满。传输服务器 Full gc 的频次过高。由于我们是使用 Ruby 来实现的过程,其内存默认设置的数据量有时会过大。存储服务器出现单波峰现象,即存储服务器磁盘有时会突然出现性能直线骤升或骤降。频繁触发高水位。如前所述的高水位保护机制,一旦存储磁盘触发了高水位,则不再提供服务,只能等待人工进行磁盘“清洗”。如果 ES 的一台机器“挂”了,则集群就 hang 住了。即当发现某台机器无法通讯时,集群会认为它“挂”了,则快速启动数据恢复。而如果正值系统繁忙之时,则此类数据恢复的操作会更加拖累系统的整体性能。由于所有数据都被写入 Kafka,而我们只用到了一个 topic,这就造成了每一类数据都要经过不一定与之相关的规则链,并进行不一定适用的规则判断,因此数据的传输效率整体被降低了。Fluentd 的 host 轮询机制造成高水位频发。由于 Fluentd 在与 ES 对接时遵循一个默认策略:首选前五台进行数据写入,即与前五台的前五个接口交互。在我们的生产环境中,Fluentd 是用 CRuby 写的。每一个进程属于一个 Fluentd 进程,且每一个进程都会对应一个 host 文件。而该 host 文件的前五个默认值即为 ES 的写入入口,因此所有机器都会去找这五个入口。倘若有一台机器宕机,则会轮询到下一台。如此直接造成了高水位的频繁出现、和写入速度的下降。众所周知,对日志的查询是一种低频次的查询,即只有在出现问题时才会去查看。但是在实际操作中,我们往往通过检索的方式全部取出,因此意义不大。另外 ES 为了达到较好的性能,会将数据存储在 raid0 中,存储的时间跨度往往会超过 7 天,因此其成本也比较高。通过对数据的实时线分析,我们发现并未达到写入/写出的平衡状态。为了提高 Fluentd 的利用率,我们用 Kafka 去数据的时候提高了量,原来是 5 兆,现在我们改到了 6 兆。如果只是单纯传输,不论计算的话,其实可以改更高。只不过因为我们考虑到这里包含了计算的一些东西,所以只提到了 6 兆。我们的 Fluentd 是基于 JRuby 的,因为 JRuby 可以多线程,但是我们的 CRuby 没有任何意义。为了提高内存,我把 Ruby 所有的内存机制了解了一下,就是散列的一些 host 文件,因为我们每个进程都选前五列就可以了,我多开了几个口。ES 的优化这一块,在上 ES 之前,我们已经有人做过一次优化了。因为基于我刚才说的有时候日志量很高,有时候日志量很少。我们会考虑做动态配置。因为 ES 就是支持动态配置的,所以它动态配置的时候,我们在某些场景下可以提高它的写入速度,某些场景下可以支持它的这种查询效率。我们可以尝试去做一些动态配置负载。改造一:存储降低降低存储在整体架构上并没有太大变化,我们只是在传输到 Fluentd 时把天数降下来,改成了一天。同时,我们直接进行了分流,把数据往 Hadoop 里写,而把一些符合 Kibana 的数据直接放入 ES。上面提过,日志查询是低频次的,一般需要查询两天以上数据的可能性很小,因此我们降低存储是非常有意义的。改造二:数据分治我们在日志文件节点数较少(机器数量小于 5 台)的情况下,去掉了 Kafka 层。由于 Fluentd 可以支持数据和大文件存储,因此数据能够被持久化地存入磁盘。我们给每个应用都直接对应了一个 tag,以方便各个应用对应到自己的 tag、遵循自己的固定规则、并最终写入 ES,这样就方便了出现问题的各自定位。另外,我们运用延迟计算和文件切分也能快速地找到问题的根源。因此我们节约了 Kafka 和 ES 各种计算资源。在实际操作中,由于 HBase 不用去做 raid,它自己完全能够控制磁盘的写入,因此我们进行了数据压缩。就其效果而言,ES 的存储开销大幅降低。在后期,我们也尝试过一种更为极端的方案:让用户直接通过客户端的 Shell 去查询数据,并采用本地缓存的留存机制。优化效果优化的效果如下:服务器资源的有效利用。在实施了新的方案之后,我们省了很多服务器,而且单台服务器的存储资源也节省了 15%。单核处理每秒原来能够传输 3000 条,实施后提升到了 1.5~1.8 万条。而且,在服务器单独空跑,即不加任何计算时,单核每秒能传输近 3 万条。很少触发 ES 保护机制。原因就是我们已把数据分流出来了。以前历史数据只能存 7 天,由于我们节省了服务器,因此我们现在可以存储更长时间的数据。而且,对于一些他人查询过的日志,我们也会根据最初的策略,有选择性地保留下来,以便追溯。日志系统优化总结关于日志平台优化,我总结了如下几点:由于日志是低频次的,我们把历史数据存入了廉价存储之中,普通用户需要的时候,我们再导到 ES 里,通过 Kibana 的前端界面便可快速查询到。而对于程序员来说,则不需要到 ES 便可直接查询到。数据存在的时间越长,则意义越小。我们根据实际情况制定了有效的、留存有意义数据的策略。顺序写盘替代内存。例如:区别于平常的随机写盘,我们在操作读写一个流文件时采取的是按顺序写数据的模式。而在存储量大的时候,则应当考虑 SSD。特别是在 ES 遇到限流时,使用 SSD 可以提升 ES 的性能。提前定制规范,从而能够有效解决后期分析等工作。日志格式如上图所示,常用的日志格式类型包括:uuid、timestamp、host 等。特别是 host,由于日志会涉及到几百个节点,有了 host 类型,我们就能判定是哪台机器上的标准。而图中其他的环境变量类型,则能够有效地追溯到一些历史的信息。在此我向大家推荐一个架构学习交流群。交流学习群号:478030634 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多 日志方案如上图所示,我们通过 Rsyslog 可以直接将采集端的数据写入文件或数据库之中。当然,对于一些暂时用不上的日志,我们不一定非要实施过滤传输的规则。如上图,Fluentd 也有一些传输的规则,包括:Fluentd 可以直接对接 Fluentd,也可以直接对接 MongoDB、MySQL 等。另外,我们也有一些组件可以快速地对接插件和系统,例如让 Fluentd 和 Rsyslog 能够直接连到 ES 上。这是我个人给大家定制的一些最基本的基线,我认为日志从采集、缓存、传输、存储,到最终可视化,分成了三套基线。采集到存储是最简单的一个,像 Rsyslog 到 hdfs 或者其他 filesystem,我们有这种情况。比较常见的情况,就是从采集、传输、到存储可视化,然后形成最终我们现在最复杂的一套系统,大家可以根据实际情况取舍。最后是我考虑到一个实际情况,假如这个案例,我们尽可能少的占有服务器,然后传输需要过滤转换,日志可以比较简单,符合这种 Key value(KV)格式。我们可以按照取了一个 Rsyslog、取了一个 Fluentd、取了一个 Hbase,取了一个 echars 等这么一个方式做一个方案就可以了。我觉得 Rsyslog、Fluentd、heka 这些都可以做采集。然后传输这块有 Fluentd 传输,因为 Fluentd 和 Kafka 到插件非常灵活可以直接对接我们很多存储设备,也可以对应很多的文件、连 ES 都可以。可视化可以用 Kibana,主要是跟 ES 结合得比较紧密,它们结合在一起需要一点学习成本。大家觉得文章对你还是有一点点帮助的,大家可以点击下方二维码进行关注。 《乐趣区》 公众号聊的不仅仅是Java技术知识,还有面试等干货,后期还有大量架构干货。大家一起关注吧!关注烂猪皮,你会了解的更多………….. 原文连接:https://blog.csdn.net/yunzhaj… ...

October 8, 2018 · 2 min · jiezi

Springboot 2.0 - 集成redis

最近在入门SpringBoot,然后在感慨 SpringBoot较于Spring真的方便多时,顺便记录下自己在集成redis时的一些想法。从springboot官网查看redis的依赖包<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId></dependency>操作redis /* 操作k-v都是字符串的 / @Autowired StringRedisTemplate stringRedisTemplet; / 操作k-v都是对象的 /@AutowiredRedisTemplate redisTemplate;redis的包中提供了两个可以操作方法,根据不同类型的值相对应选择。两个操作方法对应的redis操作都是相同的stringRedisTemplet.opsForValue() // 字符串stringRedisTemplet.opsForList() // 列表stringRedisTemplet.opsForSet() // 集合stringRedisTemplet.opsForHash() // 哈希stringRedisTemplet.opsForZSet() // 有序集合修改数据的存储方式在StringRedisTemplet中,默认都是存储字符串的形式;在RedisTemplet中,值可以是某个对象,而redis默认把对象序列化后存储在redis中(所以存放的对象默认情况下需要序列化)如果需要更改数据的存储方式,如采用json来存储在redis中,而不是以序列化后的形式。1)自己创建一个RedisTemplate实例,在该实例中自己定义json的序列化格式(org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer)// 这里传入的是employee对象(employee 要求可以序列化)Jackson2JsonRedisSerializer<Employee> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Employee>(Employee.class);2)把定义的格式放进自己定义的RedisTemplate实例中RedisTemplate<Object,Employee> template = new RedisTemplate<>();template.setConnectionFactory(redisConnectionFactory);// 定义格式Jackson2JsonRedisSerializer<Employee> jackson2JsonRedisSerializer = newJackson2JsonRedisSerializer<Employee>(Employee.class);// 放入RedisTemplate实例中template.setDefaultSerializer(jackson2JsonRedisSerializer);参考代码:@Beanpublic RedisTemplate<Object,Employee> employeeRedisTemplate(RedisConnectionFactory redisConnectionFactory)throws UnknownHostException{ RedisTemplate<Object,Employee> template = new RedisTemplate<>(); template.setConnectionFactory(redisConnectionFactory); Jackson2JsonRedisSerializer<Employee> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Employee>(Employee.class); template.setDefaultSerializer(jackson2JsonRedisSerializer); return template;} 原理: @Configuration @ConditionalOnClass({RedisOperations.class}) @EnableConfigurationProperties({RedisProperties.class}) @Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class}) public class RedisAutoConfiguration { public RedisAutoConfiguration() { } @Bean @ConditionalOnMissingBean( name = {“redisTemplate”} ) // 在容器当前没有redisTemplate时运行 public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { RedisTemplate<Object, Object> template = new RedisTemplate(); template.setConnectionFactory(redisConnectionFactory); return template;}@Bean@ConditionalOnMissingBean // 在容器当前没有stringRedisTemplate时运行public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { StringRedisTemplate template = new StringRedisTemplate(); template.setConnectionFactory(redisConnectionFactory); return template;}}如果你自己定义了RedisTemplate后并添加@Bean注解,(要在配置类中定义),那么默认的RedisTemplate就不会被添加到容器中,运行的就是自己定义的ReidsTemplate实例,而你在实例中自己定义了序列化格式,所以就会以你采用的格式定义存放在redis中的对象。更改默认的缓冲springboot默认提供基于注解的缓冲,只要在主程序类(xxxApplication)标注@EnableCaching,缓冲注解有@Cachingable、@CachingEvict、@CachingPut,并且该缓冲默认使用的是ConcurrentHashMapCacheManager当引入redis的starter后,容器中保存的是RedisCacheManager ,RedisCacheManager创建RedisCache作为缓冲组件,RedisCache通过操纵redis缓冲数据在此我向大家推荐一个架构学习交流群。交流学习群号:478030634 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多修改redis缓冲的序列化机制在SpringBoot中,如果要修改序列化机制,可以直接建立一个配置类,在配置类中自定义CacheManager,在CacheManager中可以自定义序列化的规则,默认的序列化规则是采用jdk的序列化注:在SpringBoot 1.5.6 和SpringBoot 2.0.5 的版本中自定义CacheManager存在差异参考代码:// springboot 1.x的版本public RedisCacheManager employeeCacheManager(RedisConnectionFactory redisConnectionFactory){// 1、自定义RedisTemplateRedisTemplate<Object,Employee> template = new RedisTemplate<>();template.setConnectionFactory(redisConnectionFactory);Jackson2JsonRedisSerializer<Employee> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Employee>(Employee.class); template.setDefaultSerializer(jackson2JsonRedisSerializer);// 2、自定义RedisCacheManagerRedisCacheManager cacheManager = new RedisCacheManager(template);cacheManager.setUsePrefix(true); // 会将CacheName作为key的前缀return cacheManager;}// springboot 2.x的版本/** serializeKeysWith() 修改key的序列化规则,这里采用的是StringRedisSerializer()* serializeValuesWith() 修改value的序列化规则,这里采用的是 Jackson2JsonRedisSerializer<Employee>(Employee.class)* @param factory* @return */@Beanpublic RedisCacheManager employeeCacheManager(RedisConnectionFactory redisConnectionFactory) {RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() . serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new Jackson2JsonRedisSerializer<Employee>(Employee.class))); RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(config).build(); return cacheManager; }tip:可以通过查看各版本的org.springframework.data.redis.cache.RedisCacheConfiguration去自定义CacheManager.因为不同版本的SpringBoot对应的Redis版本也是不同的,所以要重写时可以查看官方是怎么定义CacheManager,才知道怎样去自定义CacheManager。大家觉得文章对你还是有一点点帮助的,大家可以点击下方二维码进行关注。 《 乐趣区 》 公众号聊的不仅仅是Java技术知识,还有面试等干货,后期还有大量架构干货。大家一起关注吧!关注烂猪皮,你会了解的更多………….. ...

October 5, 2018 · 1 min · jiezi

一段万能的nginx接口反向代理配置

作为前端开发,每次调试接口,把代码发到测试服务器,是很费时费事的一件事情。为了提高效率,想到了nginx反向代理来解决这一问题。接口地址:test.com访问地址:localhost最核心的问题就是,登录时,无法写入cookie的问题,为了解决这个问题,走了不少弯路。worker_processes 1;events { worker_connections 1024;}http { include mime.types; default_type application/octet-stream; sendfile on; keepalive_timeout 10; server { listen 80; server_name localhost; location =/ { add_header X-Frame-Options SAMEORIGIN; root D:/workspace/; index index.html; } location ~* .(html|htm|gif|jpg|jpeg|bmp|png|ico|txt|js|css|swf|woff|woff2|ttf|json|svg|cur|vue|otf|eot)$ { charset utf-8; root D:/workspace/; expires 3d; } location = /socket/v2 { proxy_pass http://test.com; proxy_redirect off; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection “upgrade”; proxy_set_header Host test.com; proxy_set_header X-Real-IP $remote_addr; proxy_set_header REMOTE-HOST $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_connect_timeout 30; proxy_send_timeout 30; proxy_read_timeout 60; proxy_buffer_size 256k; proxy_buffers 4 256k; } location / { proxy_pass http://test.com; proxy_set_header Cookie $http_cookie; proxy_cookie_domain test.com localhost; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host test.com; proxy_set_header X-Real-IP $remote_addr; proxy_set_header REMOTE-HOST $remote_addr; } }}核心代码在三行代码上:proxy_set_header Cookie $http_cookie;proxy_cookie_domain test.com localhost;proxy_set_header Host test.com;具体解释我也是一知半解:第一个是携带cookie,第二个设置cookie 的 domain第三个 设置真实的host重要提示:以上3个的顺序不要颠倒,否则代理失败,我也不知道为什么。如何在手机上调试呢?手机上不可能直接访问localhost,可以把手机和电脑连接到同一个网段,使用电脑的ip进行访问。但是这里只代理了localhost,并没有代理电脑的ip所以,需要把是上面的server{…}拷贝一份,只需要把里面的localhost全部改成你的电脑ip就可以了,最终代码:worker_processes 1;events { worker_connections 1024;}http { include mime.types; default_type application/octet-stream; sendfile on; keepalive_timeout 10; server { listen 80; server_name localhost; location =/ { add_header X-Frame-Options SAMEORIGIN; root D:/workspace/; index index.html; } location ~* .(html|htm|gif|jpg|jpeg|bmp|png|ico|txt|js|css|swf|woff|woff2|ttf|json|svg|cur|vue|otf|eot)$ { charset utf-8; root D:/workspace/; expires 3d; } location = /socket/v2 { proxy_pass http://test.com; proxy_redirect off; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection “upgrade”; proxy_set_header Host test.com; proxy_set_header X-Real-IP $remote_addr; proxy_set_header REMOTE-HOST $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_connect_timeout 30; proxy_send_timeout 30; proxy_read_timeout 60; proxy_buffer_size 256k; proxy_buffers 4 256k; } location / { proxy_pass http://test.com; proxy_set_header Cookie $http_cookie; proxy_cookie_domain test.com localhost; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host test.com; proxy_set_header X-Real-IP $remote_addr; proxy_set_header REMOTE-HOST $remote_addr; } } server { listen 8080; server_name xx.xx.xx.xx; location =/ { add_header X-Frame-Options SAMEORIGIN; root D:/workspace/; index index.html; } location ~* .(html|htm|gif|jpg|jpeg|bmp|png|ico|txt|js|css|swf|woff|woff2|ttf|json|svg|cur|vue|otf|eot)$ { charset utf-8; root D:/workspace/; expires 3d; } location = /socket/v2 { proxy_pass http://test.com; proxy_redirect off; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection “upgrade”; proxy_set_header Host test.com; proxy_set_header X-Real-IP $remote_addr; proxy_set_header REMOTE-HOST $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_connect_timeout 30; proxy_send_timeout 30; proxy_read_timeout 60; proxy_buffer_size 256k; proxy_buffers 4 256k; } location / { proxy_pass http://test.com; proxy_set_header Cookie $http_cookie; proxy_cookie_domain test.com xx.xx.xx.xx; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host test.com; proxy_set_header X-Real-IP $remote_addr; proxy_set_header REMOTE-HOST $remote_addr; } }}访问方法:http://xx.xx.xx.xx:8080 即可如果是打包工具生成增这个配置的话,可以用nodejs动态获取你电脑的ipfunction getIPAdress() { var interfaces = require(‘os’).networkInterfaces(); for (var devName in interfaces) { var iface = interfaces[devName]; for (var i = 0; i < iface.length; i++) { var alias = iface[i]; if (alias.family === ‘IPv4’ && alias.address !== ‘127.0.0.1’ && !alias.internal) { return alias.address; } } } }所以,这里贴出来一个动态生成nginx.config的工具function buildNginxConfig(config) { function getIPAdress() { var interfaces = require(‘os’).networkInterfaces(); for (var devName in interfaces) { var iface = interfaces[devName]; for (var i = 0; i < iface.length; i++) { var alias = iface[i]; if (alias.family === ‘IPv4’ && alias.address !== ‘127.0.0.1’ && !alias.internal) { return alias.address; } } } } var cwd = process.cwd().replace(/\/g, ‘/’) + ‘/app’; var protocol = /https|443/.test(config.ip) ? ‘https’ : ‘http’; var servers = [{ browserIp: ’localhost’, port: 80, root: cwd, serverIp: config.ip, protocol: protocol, }, { browserIp: getIPAdress(), port: 8080, root: cwd, serverIp: config.ip, protocol: protocol, }].map(function(item) { return server { listen ${item.port}; server_name ${item.browserIp}; location =/ { add_header X-Frame-Options SAMEORIGIN; root ${item.root}; index index.html; } location ~* \\.(html|htm|gif|jpg|jpeg|bmp|png|ico|txt|js|css|swf|woff|woff2|ttf|json|svg|cur|vue|otf|eot)$ { charset utf-8; root ${item.root}; expires 3d; } location = /socket/v2 { proxy_pass ${item.protocol}://${item.serverIp}; proxy_redirect off; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host ${item.serverIp}; proxy_set_header X-Real-IP $remote_addr; proxy_set_header REMOTE-HOST $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_connect_timeout 30; proxy_send_timeout 30; proxy_read_timeout 60; proxy_buffer_size 256k; proxy_buffers 4 256k; } location / { proxy_pass ${item.protocol}://${item.serverIp}; proxy_set_header Cookie $http_cookie; proxy_cookie_domain ${item.serverIp} ${item.browserIp}; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host ${item.serverIp}; proxy_set_header X-Real-IP $remote_addr; proxy_set_header REMOTE-HOST $remote_addr; } }; }).join(’\n’); var str = worker_processes 1;events { worker_connections 1024;}http { include mime.types; default_type application/octet-stream; sendfile on; keepalive_timeout 10; ${servers}}; return str;}exports = module.exports = buildNginxConfig;有了这个万能反向代理,可以随心所欲的玩转任何网站接口了 ...

October 1, 2018 · 3 min · jiezi

基于 Spring Cloud 完整的微服务架构实战

技术栈Spring boot - 微服务的入门级微框架,用来简化 Spring 应用的初始搭建以及开发过程。Eureka - 云端服务发现,一个基于 REST 的服务,用于定位服务,以实现云端中间层服务发现和故障转移。Spring Cloud Config - 配置管理工具包,让你可以把配置放到远程服务器,集中化管理集群配置,目前支持本地存储、Git 以及 Subversion。Hystrix - 熔断器,容错管理工具,旨在通过熔断机制控制服务和第三方库的节点,从而对延迟和故障提供更强大的容错能力。Zuul - Zuul 是在云平台上提供动态路由,监控,弹性,安全等边缘服务的框架。Zuul 相当于是设备和 Netflix 流应用的 Web 网站后端所有请求的前门。Spring Cloud Bus - 事件、消息总线,用于在集群(例如,配置变化事件)中传播状态变化,可与 Spring Cloud Config 联合实现热部署。Spring Cloud Sleuth - 日志收集工具包,封装了 Dapper 和 log-based 追踪以及 Zipkin 和 HTrace 操作,为 SpringCloud 应用实现了一种分布式追踪解决方案。Ribbon - 提供云端负载均衡,有多种负载均衡策略可供选择,可配合服务发现和断路器使用。Turbine - Turbine 是聚合服务器发送事件流数据的一个工具,用来监控集群下 hystrix 的 metrics 情况。Spring Cloud Stream - Spring 数据流操作开发包,封装了与 Redis、Rabbit、Kafka 等发送接收消息。Feign - Feign 是一种声明式、模板化的 HTTP 客户端。Spring Cloud OAuth2 - 基于 Spring Security 和 OAuth2 的安全工具包,为你的应用程序添加安全控制。应用架构该项目包含 8 个服务registry - 服务注册与发现config - 外部配置monitor - 监控zipkin - 分布式跟踪gateway - 代理所有微服务的接口网关auth-service - OAuth2 认证服务svca-service - 业务服务Asvcb-service - 业务服务B体系架构技术栈Spring boot - 微服务的入门级微框架,用来简化 Spring 应用的初始搭建以及开发过程。Eureka - 云端服务发现,一个基于 REST 的服务,用于定位服务,以实现云端中间层服务发现和故障转移。Spring Cloud Config - 配置管理工具包,让你可以把配置放到远程服务器,集中化管理集群配置,目前支持本地存储、Git 以及 Subversion。Hystrix - 熔断器,容错管理工具,旨在通过熔断机制控制服务和第三方库的节点,从而对延迟和故障提供更强大的容错能力。Zuul - Zuul 是在云平台上提供动态路由,监控,弹性,安全等边缘服务的框架。Zuul 相当于是设备和 Netflix 流应用的 Web 网站后端所有请求的前门。Spring Cloud Bus - 事件、消息总线,用于在集群(例如,配置变化事件)中传播状态变化,可与 Spring Cloud Config 联合实现热部署。Spring Cloud Sleuth - 日志收集工具包,封装了 Dapper 和 log-based 追踪以及 Zipkin 和 HTrace 操作,为 SpringCloud 应用实现了一种分布式追踪解决方案。Ribbon - 提供云端负载均衡,有多种负载均衡策略可供选择,可配合服务发现和断路器使用。Turbine - Turbine 是聚合服务器发送事件流数据的一个工具,用来监控集群下 hystrix 的 metrics 情况。Spring Cloud Stream - Spring 数据流操作开发包,封装了与 Redis、Rabbit、Kafka 等发送接收消息。Feign - Feign 是一种声明式、模板化的 HTTP 客户端。Spring Cloud OAuth2 - 基于 Spring Security 和 OAuth2 的安全工具包,为你的应用程序添加安全控制。应用架构该项目包含 8 个服务registry - 服务注册与发现config - 外部配置monitor - 监控zipkin - 分布式跟踪gateway - 代理所有微服务的接口网关auth-service - OAuth2 认证服务svca-service - 业务服务Asvcb-service - 业务服务B体系架构技术栈Spring boot - 微服务的入门级微框架,用来简化 Spring 应用的初始搭建以及开发过程。Eureka - 云端服务发现,一个基于 REST 的服务,用于定位服务,以实现云端中间层服务发现和故障转移。Spring Cloud Config - 配置管理工具包,让你可以把配置放到远程服务器,集中化管理集群配置,目前支持本地存储、Git 以及 Subversion。Hystrix - 熔断器,容错管理工具,旨在通过熔断机制控制服务和第三方库的节点,从而对延迟和故障提供更强大的容错能力。Zuul - Zuul 是在云平台上提供动态路由,监控,弹性,安全等边缘服务的框架。Zuul 相当于是设备和 Netflix 流应用的 Web 网站后端所有请求的前门。Spring Cloud Bus - 事件、消息总线,用于在集群(例如,配置变化事件)中传播状态变化,可与 Spring Cloud Config 联合实现热部署。Spring Cloud Sleuth - 日志收集工具包,封装了 Dapper 和 log-based 追踪以及 Zipkin 和 HTrace 操作,为 SpringCloud 应用实现了一种分布式追踪解决方案。Ribbon - 提供云端负载均衡,有多种负载均衡策略可供选择,可配合服务发现和断路器使用。Turbine - Turbine 是聚合服务器发送事件流数据的一个工具,用来监控集群下 hystrix 的 metrics 情况。Spring Cloud Stream - Spring 数据流操作开发包,封装了与 Redis、Rabbit、Kafka 等发送接收消息。Feign - Feign 是一种声明式、模板化的 HTTP 客户端。Spring Cloud OAuth2 - 基于 Spring Security 和 OAuth2 的安全工具包,为你的应用程序添加安全控制。应用架构该项目包含 8 个服务registry - 服务注册与发现config - 外部配置monitor - 监控zipkin - 分布式跟踪gateway - 代理所有微服务的接口网关auth-service - OAuth2 认证服务svca-service - 业务服务Asvcb-service - 业务服务B体系架构技术栈Spring boot - 微服务的入门级微框架,用来简化 Spring 应用的初始搭建以及开发过程。Eureka - 云端服务发现,一个基于 REST 的服务,用于定位服务,以实现云端中间层服务发现和故障转移。Spring Cloud Config - 配置管理工具包,让你可以把配置放到远程服务器,集中化管理集群配置,目前支持本地存储、Git 以及 Subversion。Hystrix - 熔断器,容错管理工具,旨在通过熔断机制控制服务和第三方库的节点,从而对延迟和故障提供更强大的容错能力。Zuul - Zuul 是在云平台上提供动态路由,监控,弹性,安全等边缘服务的框架。Zuul 相当于是设备和 Netflix 流应用的 Web 网站后端所有请求的前门。Spring Cloud Bus - 事件、消息总线,用于在集群(例如,配置变化事件)中传播状态变化,可与 Spring Cloud Config 联合实现热部署。Spring Cloud Sleuth - 日志收集工具包,封装了 Dapper 和 log-based 追踪以及 Zipkin 和 HTrace 操作,为 SpringCloud 应用实现了一种分布式追踪解决方案。Ribbon - 提供云端负载均衡,有多种负载均衡策略可供选择,可配合服务发现和断路器使用。Turbine - Turbine 是聚合服务器发送事件流数据的一个工具,用来监控集群下 hystrix 的 metrics 情况。Spring Cloud Stream - Spring 数据流操作开发包,封装了与 Redis、Rabbit、Kafka 等发送接收消息。Feign - Feign 是一种声明式、模板化的 HTTP 客户端。Spring Cloud OAuth2 - 基于 Spring Security 和 OAuth2 的安全工具包,为你的应用程序添加安全控制。应用架构该项目包含 8 个服务registry - 服务注册与发现config - 外部配置monitor - 监控zipkin - 分布式跟踪gateway - 代理所有微服务的接口网关auth-service - OAuth2 认证服务svca-service - 业务服务Asvcb-service - 业务服务B体系架构应用组件启动项目使用 Docker 快速启动配置 Docker 环境mvn clean package 打包项目及 Docker 镜像在项目根目录下执行 docker-compose up -d 启动所有项目本地手动启动配置 rabbitmq修改 hosts 将主机名指向到本地127.0.0.1 registry config monitor rabbitmq auth-service或者修改各服务配置文件中的相应主机名为本地 ip启动 registry、config、monitor、zipkin启动 gateway、auth-service、svca-service、svcb-service项目预览注册中心访问 http://localhost:8761/ 默认账号 user,密码 password监控访问 http://localhost:8040/ 默认账号 admin,密码 admin控制面板bp.jpg应用注册历史Turbine Hystrix面板应用信息、健康状况、垃圾回收等详情计数器查看和修改环境变量管理 Logback 日志级别查看并使用 JMX查看线程认证历史查看 Http 请求轨迹Hystrix 面板链路跟踪访问 http://localhost:9411/ 默认账号 admin,密码 admin控制面板链路跟踪明细服务依赖关系RabbitMQ 监控Docker 启动访问 http://localhost:15673/ 默认账号 guest,密码 guest(本地 rabbit 管理系统默认端口15672)接口测试在此我向大家推荐一个架构学习交流群。交流学习群号:897889510 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多接口测试1.获取 Tokencurl -X POST -vu client:secret http://localhost:8060/uaa/oauth/token -H “Accept: application/json” -d “password=password&username=anil&grant_type=password&scope=read%20write"返回如下格式数据:{“access_token”: “eac56504-c4f0-4706-b72e-3dc3acdf45e9”,“token_type”: “bearer”,“refresh_token”: “da1007dc-683c-4309-965d-370b15aa4aeb”,“expires_in”: 3599,“scope”: “read write”}2.使用 access token 访问 service a 接口curl -i -H “Authorization: Bearer eac56504-c4f0-4706-b72e-3dc3acdf45e9” http://localhost:8060/svca 返回如下数据:svca-service (172.18.0.8:8080)===>name:zhangxdsvcb-service (172.18.0.2:8070)===>Say Hello 3.使用 access token 访问 service b 接口curl -i -H “Authorization: Bearer eac56504-c4f0-4706-b72e-3dc3acdf45e9” http://localhost:8060/svcb 返回如下数据:svcb-service (172.18.0.2:8070)===>Say Hello 4.使用 refresh token 刷新 tokencurl -X POST -vu client:secret http://localhost:8060/uaa/oauth/token -H “Accept: application/json” -d “grant_type=refresh_token&refresh_token=da1007dc-683c-4309-965d-370b15aa4aeb” 返回更新后的 Token:{“access_token”: “63ff57ce-f140-482e-ba7e-b6f29df35c88”,“token_type”: “bearer”,“refresh_token”: “da1007dc-683c-4309-965d-370b15aa4aeb”,“expires_in”: 3599,“scope”: “read write”} 5.刷新配置curl -X POST -vu user:password http://localhost:8888/bus/refresh ...

September 30, 2018 · 3 min · jiezi

消息中间件—简谈Kafka中的NIO网络通信模型

摘要:很多人喜欢把RocketMQ与Kafka做对比,其实这两款消息队列的网络通信层还是比较相似的,本文就为大家简要地介绍下Kafka的NIO网络通信模型,通过对Kafka源码的分析来简述其Reactor的多线程网络通信模型和总体框架结构,同时简要介绍Kafka网络通信层的设计与具体实现。一、Kafka网络通信模型的整体框架概述Kafka的网络通信模型是基于NIO的Reactor多线程模型来设计的。这里先引用Kafka源码中注释的一段话:相信大家看了上面的这段引文注释后,大致可以了解到Kafka的网络通信层模型,主要采用了 1(1个Acceptor线程)+N(N个Processor线程)+M(M个业务处理线程) 。下面的表格简要的列举了下(这里先简单的看下后面还会详细说明):线程数线程名线程具体说明1kafka-socket-acceptor_%xAcceptor线程,负责监听Client端发起的请求Nkafka-network-thread_%dProcessor线程,负责对Socket进行读写Mkafka-request-handler-_%dWorker线程,处理具体的业务逻辑并生成Response返回Kafka网络通信层的完整框架图如下图所示:Kafka消息队列的通信层模型—1+N+M模型.png刚开始看到上面的这个框架图可能会有一些不太理解,并不要紧,这里可以先对Kafka的网络通信层框架结构有一个大致了解。本文后面会结合Kafka的部分重要源码来详细阐述上面的过程。这里可以简单总结一下其网络通信模型中的几个重要概念:(1), Acceptor :1个接收线程,负责监听新的连接请求,同时注册OP_ACCEPT 事件,将新的连接按照 “round robin” 方式交给对应的 Processor 线程处理;(2), Processor :N个处理器线程,其中每个 Processor 都有自己的 selector,它会向 Acceptor 分配的 SocketChannel 注册相应的 OP_READ 事件,N 的大小由 “num.networker.threads” 决定;(3), KafkaRequestHandler :M个请求处理线程,包含在线程池—KafkaRequestHandlerPool内部,从RequestChannel的全局请求队列—requestQueue中获取请求数据并交给KafkaApis处理,M的大小由 “num.io.threads” 决定;(4), RequestChannel :其为Kafka服务端的请求通道,该数据结构中包含了一个全局的请求队列 requestQueue和多个与Processor处理器相对应的响应队列responseQueue,提供给Processor与请求处理线程KafkaRequestHandler和KafkaApis交换数据的地方。(5), NetworkClient :其底层是对 Java NIO 进行相应的封装,位于Kafka的网络接口层。Kafka消息生产者对象—KafkaProducer的send方法主要调用NetworkClient完成消息发送;(6), SocketServer :其是一个NIO的服务,它同时启动一个Acceptor接收线程和多个Processor处理器线程。提供了一种典型的Reactor多线程模式,将接收客户端请求和处理请求相分离;(7), KafkaServer :代表了一个Kafka Broker的实例;其startup方法为实例启动的入口;(8), KafkaApis :Kafka的业务逻辑处理Api,负责处理不同类型的请求;比如 “发送消息”、 “获取消息偏移量—offset” 和 “处理心跳请求” 等;二、Kafka网络通信层的设计与具体实现这一节将结合Kafka网络通信层的源码来分析其设计与实现,这里主要详细介绍网络通信层的几个重要元素—SocketServer、Acceptor、Processor、RequestChannel和KafkaRequestHandler。本文分析的源码部分均基于Kafka的0.11.0版本。1、SocketServerSocketServer是接收客户端Socket请求连接、处理请求并返回处理结果的核心类,Acceptor及Processor的初始化、处理逻辑都是在这里实现的。在KafkaServer实例启动时会调用其startup的初始化方法,会初始化1个 Acceptor和N个Processor线程(每个EndPoint都会初始化,一般来说一个Server只会设置一个端口),其实现如下:2、AcceptorAcceptor是一个继承自抽象类AbstractServerThread的线程类。Acceptor的主要任务是监听并且接收客户端的请求,同时建立数据传输通道—SocketChannel,然后以轮询的方式交给一个后端的Processor线程处理(具体的方式是添加socketChannel至并发队列并唤醒Processor线程处理)。在该线程类中主要可以关注以下两个重要的变量:(1), nioSelector :通过NSelector.open()方法创建的变量,封装了JAVA NIO Selector的相关操作;(2), serverChannel :用于监听端口的服务端Socket套接字对象;下面来看下Acceptor主要的run方法的源码:在上面源码中可以看到,Acceptor线程启动后,首先会向用于监听端口的服务端套接字对象—ServerSocketChannel上注册OP_ACCEPT 事件。然后以轮询的方式等待所关注的事件发生。如果该事件发生,则调用accept()方法对OP_ACCEPT事件进行处理。这里,Processor是通过 round robin 方法选择的,这样可以保证后面多个Processor线程的负载基本均匀。Acceptor的accept()方法的作用主要如下:(1)通过SelectionKey取得与之对应的serverSocketChannel实例,并调用它的accept()方法与客户端建立连接;(2)调用connectionQuotas.inc()方法增加连接统计计数;并同时设置第(1)步中创建返回的socketChannel属性(如sendBufferSize、KeepAlive、TcpNoDelay、configureBlocking等)(3)将socketChannel交给processor.accept()方法进行处理。这里主要是将socketChannel加入Processor处理器的并发队列newConnections队列中,然后唤醒Processor线程从队列中获取socketChannel并处理。其中,newConnections会被Acceptor线程和Processor线程并发访问操作,所以newConnections是ConcurrentLinkedQueue队列(一个基于链接节点的无界线程安全队列)3、ProcessorProcessor同Acceptor一样,也是一个线程类,继承了抽象类AbstractServerThread。其主要是从客户端的请求中读取数据和将KafkaRequestHandler处理完响应结果返回给客户端。在该线程类中主要关注以下几个重要的变量:(1), newConnections :在上面的 Acceptor 一节中已经提到过,它是一种ConcurrentLinkedQueue[SocketChannel]类型的队列,用于保存新连接交由Processor处理的socketChannel;(2), inflightResponses :是一个Map[String, RequestChannel.Response]类型的集合,用于记录尚未发送的响应;(3), selector :是一个类型为KSelector变量,用于管理网络连接;下面先给出Processor处理器线程run方法执行的流程图:Kafk_Processor线程的处理流程图.png从上面的流程图中能够可以看出Processor处理器线程在其主流程中主要完成了这样子几步操作:(1), 处理newConnections队列中的socketChannel 。遍历取出队列中的每个socketChannel并将其在selector上注册OP_READ事件;(2), 处理RequestChannel中与当前Processor对应响应队列中的Response 。在这一步中会根据responseAction的类型(NoOpAction/SendAction/CloseConnectionAction)进行判断,若为“NoOpAction”,表示该连接对应的请求无需响应;若为“SendAction”,表示该Response需要发送给客户端,则会通过“selector.send”注册OP_WRITE事件,并且将该Response从responseQueue响应队列中移至inflightResponses集合中;“CloseConnectionAction”,表示该连接是要关闭的;(3), 调用selector.poll()方法进行处理 。该方法底层即为调用nioSelector.select()方法进行处理。(4), 处理已接受完成的数据包队列—completedReceives 。在processCompletedReceives方法中调用“requestChannel.sendRequest”方法将请求Request添加至requestChannel的全局请求队列—requestQueue中,等待KafkaRequestHandler来处理。同时,调用“selector.mute”方法取消与该请求对应的连接通道上的OP_READ事件;(5), 处理已发送完的队列—completedSends 。当已经完成将response发送给客户端,则将其从inflightResponses移除,同时通过调用“selector.unmute”方法为对应的连接通道重新注册OP_READ事件;(6), 处理断开连接的队列 。将该response从inflightResponses集合中移除,同时将connectionQuotas统计计数减1;4、RequestChannel在Kafka的网络通信层中,RequestChannel为Processor处理器线程与KafkaRequestHandler线程之间的数据交换提供了一个数据缓冲区,是通信过程中Request和Response缓存的地方。因此,其作用就是在通信中起到了一个数据缓冲队列的作用。Processor线程将读取到的请求添加至RequestChannel的全局请求队列—requestQueue中;KafkaRequestHandler线程从请求队列中获取并处理,处理完以后将Response添加至RequestChannel的响应队列—responseQueue中,并通过responseListeners唤醒对应的Processor线程,最后Processor线程从响应队列中取出后发送至客户端。5、KafkaRequestHandlerKafkaRequestHandler也是一种线程类,在KafkaServer实例启动时候会实例化一个线程池—KafkaRequestHandlerPool对象(包含了若干个KafkaRequestHandler线程),这些线程以守护线程的方式在后台运行。在KafkaRequestHandler的run方法中会循环地从RequestChannel中阻塞式读取request,读取后再交由KafkaApis来具体处理。6、KafkaApisKafkaApis是用于处理对通信网络传输过来的业务消息请求的中心转发组件。该组件反映出Kafka Broker Server可以提供哪些服务。三、总结仔细阅读Kafka的NIO网络通信层的源码过程中还是可以收获不少关于NIO网络通信模块的关键技术。Apache的任何一款开源中间件都有其设计独到之处,值得借鉴和学习。对于任何一位使用Kafka这款分布式消息队列的同学来说,如果能够在一定实践的基础上,再通过阅读其源码能起到更为深入理解的效果,对于大规模Kafka集群的性能调优和问题定位都大有裨益。对于刚接触Kafka的同学来说,想要自己掌握其NIO网络通信层模型的关键设计,还需要不断地使用本地环境进行debug调试和阅读源码反复思考。 ...

September 29, 2018 · 1 min · jiezi

dubbo负载均衡策略及对应源码分析

在集群负载均衡时,Dubbo 提供了多种均衡策略,缺省为 random 随机调用。我们还可以扩展自己的负责均衡策略,前提是你已经从一个小白变成了大牛,嘻嘻1、Random LoadBalance1.1 随机,按权重设置随机概率。1.2 在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。1.3 源码分析 package com.alibaba.dubbo.rpc.cluster.loadbalance; import java.util.List; import java.util.Random; import com.alibaba.dubbo.common.URL; import com.alibaba.dubbo.rpc.Invocation; import com.alibaba.dubbo.rpc.Invoker; /** * random load balance. * * @author qianlei * @author william.liangf / public class RandomLoadBalance extends AbstractLoadBalance { public static final String NAME = “random”; private final Random random = new Random(); protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) { int length = invokers.size(); // 总个数 int totalWeight = 0; // 总权重 boolean sameWeight = true; // 权重是否都一样 for (int i = 0; i < length; i++) { int weight = getWeight(invokers.get(i), invocation); totalWeight += weight; // 累计总权重 if (sameWeight && i > 0 && weight != getWeight(invokers.get(i - 1), invocation)) { sameWeight = false; // 计算所有权重是否一样 } } if (totalWeight > 0 && ! sameWeight) { // 如果权重不相同且权重大于0则按总权重数随机 int offset = random.nextInt(totalWeight); // 并确定随机值落在哪个片断上 for (int i = 0; i < length; i++) { offset -= getWeight(invokers.get(i), invocation); if (offset < 0) { return invokers.get(i); } } } // 如果权重相同或权重为0则均等随机 return invokers.get(random.nextInt(length));}}说明:从源码可以看出随机负载均衡的策略分为两种情况a. 如果总权重大于0并且权重不相同,就生成一个1totalWeight(总权重数)的随机数,然后再把随机数和所有的权重值一一相减得到一个新的随机数,直到随机 数小于0,那么此时访问的服务器就是使得随机数小于0的权重所在的机器b. 如果权重相同或者总权重数为0,就生成一个1length(权重的总个数)的随机数,此时所访问的机器就是这个随机数对应的权重所在的机器2、RoundRobin LoadBalance2.1 轮循,按公约后的权重设置轮循比率。2.2 存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。2.3 源码分析 package com.alibaba.dubbo.rpc.cluster.loadbalance; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import com.alibaba.dubbo.common.URL; import com.alibaba.dubbo.common.utils.AtomicPositiveInteger; import com.alibaba.dubbo.rpc.Invocation; import com.alibaba.dubbo.rpc.Invoker; /* * Round robin load balance. * * @author qian.lei * @author william.liangf / public class RoundRobinLoadBalance extends AbstractLoadBalance {public static final String NAME = “roundrobin”; private final ConcurrentMap<String, AtomicPositiveInteger> sequences = new ConcurrentHashMap<String, AtomicPositiveInteger>();private final ConcurrentMap<String, AtomicPositiveInteger> weightSequences = new ConcurrentHashMap<String, AtomicPositiveInteger>();protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) { String key = invokers.get(0).getUrl().getServiceKey() + “.” + invocation.getMethodName(); int length = invokers.size(); // 总个数 int maxWeight = 0; // 最大权重 int minWeight = Integer.MAX_VALUE; // 最小权重 for (int i = 0; i < length; i++) { int weight = getWeight(invokers.get(i), invocation); maxWeight = Math.max(maxWeight, weight); // 累计最大权重 minWeight = Math.min(minWeight, weight); // 累计最小权重 } if (maxWeight > 0 && minWeight < maxWeight) { // 权重不一样 AtomicPositiveInteger weightSequence = weightSequences.get(key); if (weightSequence == null) { weightSequences.putIfAbsent(key, new AtomicPositiveInteger()); weightSequence = weightSequences.get(key); } int currentWeight = weightSequence.getAndIncrement() % maxWeight; List<Invoker<T>> weightInvokers = new ArrayList<Invoker<T>>(); for (Invoker<T> invoker : invokers) { // 筛选权重大于当前权重基数的Invoker if (getWeight(invoker, invocation) > currentWeight) { weightInvokers.add(invoker); } } int weightLength = weightInvokers.size(); if (weightLength == 1) { return weightInvokers.get(0); } else if (weightLength > 1) { invokers = weightInvokers; length = invokers.size(); } } AtomicPositiveInteger sequence = sequences.get(key); if (sequence == null) { sequences.putIfAbsent(key, new AtomicPositiveInteger()); sequence = sequences.get(key); } // 取模轮循 return invokers.get(sequence.getAndIncrement() % length);}}说明:从源码可以看出轮循负载均衡的算法是:a. 如果权重不一样时,获取一个当前的权重基数,然后从权重集合中筛选权重大于当前权重基数的集合,如果筛选出的集合的长度为1,此时所访问的机 器就是集合里面的权重对应的机器b. 如果权重一样时就取模轮循3、LeastActive LoadBalance3.1 最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差(调用前的时刻减去响应后的时刻的值)。3.2 使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大3.3 对应的源码package com.alibaba.dubbo.rpc.cluster.loadbalance;import java.util.List;import java.util.Random;import com.alibaba.dubbo.common.Constants;import com.alibaba.dubbo.common.URL;import com.alibaba.dubbo.rpc.Invocation;import com.alibaba.dubbo.rpc.Invoker;import com.alibaba.dubbo.rpc.RpcStatus;/** LeastActiveLoadBalance* * @author william.liangf*/public class LeastActiveLoadBalance extends AbstractLoadBalance {public static final String NAME = “leastactive”;private final Random random = new Random();protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) { int length = invokers.size(); // 总个数 int leastActive = -1; // 最小的活跃数 int leastCount = 0; // 相同最小活跃数的个数 int[] leastIndexs = new int[length]; // 相同最小活跃数的下标 int totalWeight = 0; // 总权重 int firstWeight = 0; // 第一个权重,用于于计算是否相同 boolean sameWeight = true; // 是否所有权重相同 for (int i = 0; i < length; i++) { Invoker<T> invoker = invokers.get(i); int active = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName()).getActive(); // 活跃数 int weight = invoker.getUrl().getMethodParameter(invocation.getMethodName(), Constants.WEIGHT_KEY, Constants.DEFAULT_WEIGHT); // 权重 if (leastActive == -1 || active < leastActive) { // 发现更小的活跃数,重新开始 leastActive = active; // 记录最小活跃数 leastCount = 1; // 重新统计相同最小活跃数的个数 leastIndexs[0] = i; // 重新记录最小活跃数下标 totalWeight = weight; // 重新累计总权重 firstWeight = weight; // 记录第一个权重 sameWeight = true; // 还原权重相同标识 } else if (active == leastActive) { // 累计相同最小的活跃数 leastIndexs[leastCount ++] = i; // 累计相同最小活跃数下标 totalWeight += weight; // 累计总权重 // 判断所有权重是否一样 if (sameWeight && i > 0 && weight != firstWeight) { sameWeight = false; } } } // assert(leastCount > 0) if (leastCount == 1) { // 如果只有一个最小则直接返回 return invokers.get(leastIndexs[0]); } if (! sameWeight && totalWeight > 0) { // 如果权重不相同且权重大于0则按总权重数随机 int offsetWeight = random.nextInt(totalWeight); // 并确定随机值落在哪个片断上 for (int i = 0; i < leastCount; i++) { int leastIndex = leastIndexs[i]; offsetWeight -= getWeight(invokers.get(leastIndex), invocation); if (offsetWeight <= 0) return invokers.get(leastIndex); } } // 如果权重相同或权重为0则均等随机 return invokers.get(leastIndexs[random.nextInt(leastCount)]);}}说明:源码里面的注释已经很清晰了,大致的意思就是活跃数越小的的机器分配到的请求越多4、ConsistentHash LoadBalance4.1 一致性 Hash,相同参数的请求总是发到同一提供者。4.2 当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。4.3 缺省只对第一个参数 Hash,如果要修改,请配置 <dubbo:parameter key=“hash.arguments” value=“0,1” />4.4 缺省用 160 份虚拟节点,如果要修改,请配置 <dubbo:parameter key=“hash.nodes” value=“320” />4.5 源码分析暂时还没有弄懂,后面弄懂了再补充进来,有兴趣的小伙伴可以自己去看一下源码,然后一起交流一下心得5、dubbo官方的文档的负载均衡配置示例服务端服务级别 <dubbo:service interface="…" loadbalance=“roundrobin” /> 客户端服务级别 <dubbo:reference interface="…" loadbalance=“roundrobin” /> 服务端方法级别 <dubbo:service interface="…"> <dubbo:method name="…" loadbalance=“roundrobin”/> </dubbo:service> 客户端方法级别 <dubbo:reference interface="…"> <dubbo:method name="…" loadbalance=“roundrobin”/> </dubbo:reference> 大家觉得文章对你还是有一点点帮助的,大家可以点击下方二维码进行关注。 《乐趣区》 公众号聊的不仅仅是Java技术知识,还有面试等干货,后期还有大量架构干货。大家一起关注吧!关注烂猪皮,你会了解的更多………….. 原文连接:https://www.cnblogs.com/leeSm… ...

September 29, 2018 · 4 min · jiezi

IOC 之深入理解 Spring IoC

IOC 理论IoC 全称为 Inversion of Control,翻译为 “控制反转”,它还有一个别名为 DI(Dependency Injection),即依赖注入。如何理解“控制反转”好呢?理解好它的关键在于我们需要回答如下四个问题:谁控制谁控制什么为何是反转哪些方面反转了在回答这四个问题之前,我们先看 IOC 的定义:所谓 IOC ,就是由 Spring IOC 容器来负责对象的生命周期和对象之间的关系上面这句话是整个 IoC 理论的核心。如何来理解这句话?我们引用一个例子来走阐述(看完该例子上面四个问题也就不是问题了)。已找女朋友为例(对于程序猿来说这个值得探究的问题)。一般情况下我们是如何来找女朋友的呢?首先我们需要根据自己的需求(漂亮、身材好、性格好)找一个妹子,然后到处打听她的兴趣爱好、微信、电话号码,然后各种投其所好送其所要,最后追到手。如下:/*** 年轻小伙子*/public class YoungMan {private BeautifulGirl beautifulGirl;YoungMan(){// 可能你比较牛逼,指腹为婚// beautifulGirl = new BeautifulGirl();}public void setBeautifulGirl(BeautifulGirl beautifulGirl) {this.beautifulGirl = beautifulGirl;}public static void main(String[] args){YoungMan you = new YoungMan();BeautifulGirl beautifulGirl = new BeautifulGirl(“你的各种条件”);beautifulGirl.setxxx(“各种投其所好”);// 然后你有女票了you.setBeautifulGirl(beautifulGirl);}}这就是我们通常做事的方式,如果我们需要某个对象,一般都是采用这种直接创建的方式(new BeautifulGirl()),这个过程复杂而又繁琐,而且我们必须要面对每个环节,同时使用完成之后我们还要负责销毁它,在这种情况下我们的对象与它所依赖的对象耦合在一起。其实我们需要思考一个问题?我们每次用到自己依赖的对象真的需要自己去创建吗?我们知道,我们依赖对象其实并不是依赖该对象本身,而是依赖它所提供的服务,只要在我们需要它的时候,它能够及时提供服务即可,至于它是我们主动去创建的还是别人送给我们的,其实并不是那么重要。再说了,相比于自己千辛万苦去创建它还要管理、善后而言,直接有人送过来是不是显得更加好呢?这个给我们送东西的“人” 就是 IoC,在上面的例子中,它就相当于一个婚介公司,作为一个婚介公司它管理着很多男男女女的资料,当我们需要一个女朋友的时候,直接跟婚介公司提出我们的需求,婚介公司则会根据我们的需求提供一个妹子给我们,我们只需要负责谈恋爱,生猴子就行了。你看,这样是不是很简单明了。诚然,作为婚介公司的 IoC 帮我们省略了找女朋友的繁杂过程,将原来的主动寻找变成了现在的被动接受(符合我们的要求),更加简洁轻便。你想啊,原来你还得鞍马前后,各种巴结,什么东西都需要自己去亲力亲为,现在好了,直接有人把现成的送过来,多么美妙的事情啊。所以,简单点说,IoC 的理念就是让别人为你服务,如下图(摘自Spring揭秘):在没有引入 IoC 的时候,被注入的对象直接依赖于被依赖的对象,有了 IoC 后,两者及其他们的关系都是通过 Ioc Service Provider 来统一管理维护的。被注入的对象需要什么,直接跟 IoC Service Provider 打声招呼,后者就会把相应的被依赖对象注入到被注入的对象中,从而达到 IOC Service Provider 为被注入对象服务的目的。所以 IoC 就是这么简单!原来是需要什么东西自己去拿,现在是需要什么东西让别人(IOC Service Provider)送过来现在在看上面那四个问题,答案就显得非常明显了:谁控制谁:在传统的开发模式下,我们都是采用直接 new 一个对象的方式来创建对象,也就是说你依赖的对象直接由你自己控制,但是有了 IOC 容器后,则直接由 IoC 容器来控制。所以“谁控制谁”,当然是 IoC 容器控制对象。控制什么:控制对象。为何是反转:没有 IoC 的时候我们都是在自己对象中主动去创建被依赖的对象,这是正转。但是有了 IoC 后,所依赖的对象直接由 IoC 容器创建后注入到被注入的对象中,依赖的对象由原来的主动获取变成被动接受,所以是反转。哪些方面反转了:所依赖对象的获取被反转了。妹子有了,但是如何拥有妹子呢?这也是一门学问。可能你比较牛逼,刚刚出生的时候就指腹为婚了。大多数情况我们还是会考虑自己想要什么样的妹子,所以还是需要向婚介公司打招呼的。还有一种情况就是,你根本就不知道自己想要什么样的妹子,直接跟婚介公司说,我就要一个这样的妹子。所以,IOC Service Provider 为被注入对象提供被依赖对象也有如下几种方式:构造方法注入、stter方法注入、接口注入。构造器注入构造器注入,顾名思义就是被注入的对象通过在其构造方法中声明依赖对象的参数列表,让外部知道它需要哪些依赖对象。YoungMan(BeautifulGirl beautifulGirl){this.beautifulGirl = beautifulGirl;}构造器注入方式比较直观,对象构造完毕后就可以直接使用,这就好比你出生你家里就给你指定了你媳妇。setter 方法注入对于 JavaBean 对象而言,我们一般都是通过 getter 和 setter 方法来访问和设置对象的属性。所以,当前对象只需要为其所依赖的对象提供相对应的 setter 方法,就可以通过该方法将相应的依赖对象设置到被注入对象中。如下:public class YoungMan {private BeautifulGirl beautifulGirl;public void setBeautifulGirl(BeautifulGirl beautifulGirl) {this.beautifulGirl = beautifulGirl;}}相比于构造器注入,setter 方式注入会显得比较宽松灵活些,它可以在任何时候进行注入(当然是在使用依赖对象之前),这就好比你可以先把自己想要的妹子想好了,然后再跟婚介公司打招呼,你可以要林志玲款式的,赵丽颖款式的,甚至凤姐哪款的,随意性较强。接口方式注入接口方式注入显得比较霸道,因为它需要被依赖的对象实现不必要的接口,带有侵入性。一般都不推荐这种方式。各个组件该图为 ClassPathXmlApplicationContext 的类继承体系结构,虽然只有一部分,但是它基本上包含了 IOC 体系中大部分的核心类和接口。下面我们就针对这个图进行简单的拆分和补充说明。Resource体系Resource,对资源的抽象,它的每一个实现类都代表了一种资源的访问策略,如ClasspathResource 、 URLResource ,FileSystemResource 等。有了资源,就应该有资源加载,Spring 利用 ResourceLoader 来进行统一资源加载,类图如下:BeanFactory 体系BeanFactory 是一个非常纯粹的 bean 容器,它是 IOC 必备的数据结构,其中 BeanDefinition 是她的基本结构,它内部维护着一个 BeanDefinition map ,并可根据 BeanDefinition 的描述进行 bean 的创建和管理。BeanFacoty 有三个直接子类 ListableBeanFactory、HierarchicalBeanFactory 和 AutowireCapableBeanFactory,DefaultListableBeanFactory 为最终默认实现,它实现了所有接口。Beandefinition 体系BeanDefinition 用来描述 Spring 中的 Bean 对象。BeandefinitionReader体系BeanDefinitionReader 的作用是读取 Spring 的配置文件的内容,并将其转换成 Ioc 容器内部的数据结构:BeanDefinition。ApplicationContext体系这个就是大名鼎鼎的 Spring 容器,它叫做应用上下文,与我们应用息息相关,她继承 BeanFactory,所以它是 BeanFactory 的扩展升级版,如果BeanFactory 是屌丝的话,那么 ApplicationContext 则是名副其实的高富帅。由于 ApplicationContext 的结构就决定了它与 BeanFactory 的不同,其主要区别有:继承 MessageSource,提供国际化的标准访问策略。继承 ApplicationEventPublisher ,提供强大的事件机制。扩展 ResourceLoader,可以用来加载多个 Resource,可以灵活访问不同的资源。对 Web 应用的支持。上面五个体系可以说是 Spring IoC 中最核心的部分,后面博文也是针对这五个部分进行源码分析。其实 IoC 咋一看还是挺简单的,无非就是将配置文件(暂且认为是 xml 文件)进行解析(分析 xml 谁不会啊),然后放到一个 Map 里面就差不多了,初看有道理,其实要面临的问题还是有很多的,下面就劳烦各位看客跟着 LZ 博客来一步一步揭开 Spring IoC 的神秘面纱。大家觉得文章对你还是有一点点帮助的,大家可以点击下方二维码进行关注。 《乐趣区》 公众号聊的不仅仅是Java技术知识,还有面试等干货,后期还有大量架构干货。大家一起关注吧!关注烂猪皮,你会了解的更多…………..原文:http://www.uml.org.cn/j2ee/20… ...

September 27, 2018 · 1 min · jiezi

分布式系统消息中间件——RibbitMQ的使用基础篇

前言我是在解决分布式事务的一致性问题时了解到RabbitMQ的,当时主要是要基于RabbitMQ来实现我们分布式系统之间对有事务可靠性要求的系统间通信的。关于分布式事务一致性问题及其常见的解决方案,可以看我另一篇博客。提到RabbitMQ,不难想到的几个关键字:消息中间件、消息队列。而消息队列不由让我想到,当时在大学学习操作系统这门课,消息队列不难想到生产者消费者模式。(PS:操作系统这门课程真的很好也很重要,其中的一些思想在我工作的很长一段一时间内给了我很大帮助和启发,给我提供了许多解决问题的思路。强烈建议每一个程序员都去学一学操作系统!)一 消息中间件1.1 简介消息中间件也可以称消息队列,是指用高效可靠的消息传递机制进行与平台无关的数据交流,并基于数据通信来进行分布式系统的集成。通过提供消息传递和消息队列模型,可以在分布式环境下扩展进程的通信。当下主流的消息中间件有RabbitMQ、Kafka、ActiveMQ、RocketMQ等。其能在不同平台之间进行通信,常用来屏蔽各种平台协议之间的特性,实现应用程序之间的协同。其优点在于能够在客户端和服务器之间进行同步和异步的连接,并且在任何时刻都可以将消息进行传送和转发。是分布式系统中非常重要的组件,主要用来解决应用耦合、异步通信、流量削峰等问题。1.2 作用消息中间件几大主要作用如下:解耦冗余(存储)扩展性削峰可恢复性顺序保证缓冲异步通信1.3 消息中间件的两种模式1.3.1 P2P模式P2P模式包含三个角色:消息队列(Queue),发送者(Sender),接收者(Receiver)。每个消息都被发送到一个特定的队列,接收者从队列中获取消息。队列保留着消息,直到他们被消费或超时。P2P的特点:每个消息只有一个消费者(Consumer)(即一旦被消费,消息就不再在消息队列中)发送者和接收者之间在时间上没有依赖性,也就是说当发送者发送了消息之后,不管接收者有没有正在运行它不会影响到消息被发送到队列接收者在成功接收消息之后需向队列应答成功如果希望发送的每个消息都会被成功处理的话,那么需要P2P模式1.3.2 Pub/Sub模式Pub/Sub模式包含三个角色主题(Topic),发布者(Publisher),订阅者(Subscriber) 。多个发布者将消息发送到Topic,系统将这些消息传递给多个订阅者。Pub/Sub的特点每个消息可以有多个消费者发布者和订阅者之间有时间上的依赖性。针对某个主题(Topic)的订阅者,它必须创建一个订阅者之后,才能消费发布者的消息。为了消费消息,订阅者必须保持运行的状态。如果希望发送的消息可以不被做任何处理、或者只被一个消息者处理、或者可以被多个消费者处理的话,那么可以采用Pub/Sub模型。1.4 常用中间件介绍与对比Kafka是LinkedIn开源的分布式发布-订阅消息系统,目前归属于Apache定级项目。Kafka主要特点是基于Pull的模式来处理消息消费,追求高吞吐量,一开始的目的就是用于日志收集和传输。0.8版本开始支持复制,不支持事务,对消息的重复、丢失、错误没有严格要求,适合产生大量数据的互联网服务的数据收集业务。RabbitMQ是使用Erlang语言开发的开源消息队列系统,基于AMQP协议来实现。AMQP的主要特征是面向消息、队列、路由(包括点对点和发布/订阅)、可靠性、安全。AMQP协议更多用在企业系统内,对数据一致性、稳定性和可靠性要求很高的场景,对性能和吞吐量的要求还在其次。RocketMQ是阿里开源的消息中间件,它是纯Java开发,具有高吞吐量、高可用性、适合大规模分布式系统应用的特点。RocketMQ思路起源于Kafka,但并不是Kafka的一个Copy,它对消息的可靠传输及事务性做了优化,目前在阿里集团被广泛应用于交易、充值、流计算、消息推送、日志流式处理、binglog分发等场景。RabbitMQ比Kafka可靠,kafka更适合IO高吞吐的处理,一般应用在大数据日志处理或对实时性(少量延迟),可靠性(少量丢数据)要求稍低的场景使用,比如ELK日志收集。二 RabbitMQ了解2.1 简介RabbitMQ是流行的开源消息队列系统。RabbitMQ是AMQP(高级消息队列协议)的标准实现。支持多种客户端,如:Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP等,支持AJAX,持久化。用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。是使用Erlang编写的一个开源的消息队列,本身支持很多的协议:AMQP,XMPP, SMTP, STOMP,也正是如此,使的它变的非常重量级,更适合于企业级的开发。同时实现了一个Broker构架,这意味着消息在发送给客户端时先在中心队列排队。对路由(Routing),负载均衡(Load balance)或者数据持久化都有很好的支持。其主要特点如下:可靠性灵活的路由扩展性高可用性多种协议多语言客户端管理界面插件机制2.2 概念RabbitMQ从整体上来看是一个典型的生产者消费者模型,主要负责接收、存储和转发消息。其整体模型架构如下图所示:我们先来看一个RabbitMQ的运转流程,稍后会对这个流程中所涉及到的一些概念进行详细的解释。生产者:(1)生产者连接到RabbitMQ Broker,建立一个连接( Connection)开启一个信道(Channel)(2)生产者声明一个交换器,并设置相关属性,比如交换机类型、是否持久化等(3)生产者声明一个队列井设置相关属性,比如是否排他、是否持久化、是否自动删除等(4)生产者通过路由键将交换器和队列绑定起来(5)生产者发送消息至RabbitMQ Broker,其中包含路由键、交换器等信息。(6)相应的交换器根据接收到的路由键查找相匹配的队列。(7)如果找到,则将从生产者发送过来的消息存入相应的队列中。(8)如果没有找到,则根据生产者配置的属性选择丢弃还是回退给生产者(9)关闭信道。(10)关闭连接。消费者:(1)消费者连接到RabbitMQ Broker ,建立一个连接(Connection),开启一个信道(Channel) 。(2)消费者向RabbitMQ Broker 请求消费相应队列中的消息,可能会设置相应的回调函数,(3)等待RabbitMQ Broker 回应并投递相应队列中的消息,消费者接收消息。(4)消费者确认(ack) 接收到的消息。(5)RabbitMQ 从队列中删除相应己经被确认的消息。(6)关闭信道。(7)关闭连接。2.2.1 信道这里我们主要讨论两个问题:为何要有信道?主要原因还是在于TCP连接的"昂贵"性。无论是生产者还是消费者,都需要和RabbitMQ Broker 建立连接,这个连接就是一条TCP 连接。而操作系统对于TCP连接的创建于销毁是非常昂贵的开销。假设消费者要消费消息,并根据服务需求合理调度线程,若只进行TCP连接,那么当高并发的时候,每秒可能都有成千上万的TCP连接,不仅仅是对TCP连接的浪费,也很快会超过操作系统每秒所能建立连接的数量。如果能在一条TCP连接上操作,又能保证各个线程之间的私密性就完美了,于是信道的概念出现了。信道为何?信道是建立在Connection 之上的虚拟连接。当应用程序与Rabbit Broker建立TCP连接的时候,客户端紧接着可以创建一个AMQP 信道(Channel) ,每个信道都会被指派一个唯一的D。RabbitMQ 处理的每条AMQP 指令都是通过信道完成的。信道就像电缆里的光纤束。一条电缆内含有许多光纤束,允许所有的连接通过多条光线束进行传输和接收。2.2.2 生产者消费者关于生产者消费者我们需要了解几个概念:Producer:生产者,即消息投递者一方。消息:消息一般分两个部分:消息体(payload)和标签。标签用来描述这条消息,如:一个交换器的名称或者一个路由Key,Rabbit通过解析标签来确定消息的去向,payload是消息内容可以使一个json,数组等等。Consumer:消费者,就是接收消息的一方。消费者订阅RabbitMQ的队列,当消费者消费一条消息时,只是消费消息的消息体。在消息路由的过程中,会丢弃标签,存入到队列中的只有消息体。Broker:消息中间件的服务节点。2.2.3 队列、交换器、路由key、绑定从RabbitMQ的运转流程我们可以知道生产者的消息是发布到交换器上的。而消费者则是从队列上获取消息的。那么消息到底是如何从交换器到队列的呢?我们先具体了解一下这几个概念。Queue:队列,是RabbitMQ的内部对象,用于存储消息。RabbitMQ中消息只能存储在队列中。生产者投递消息到队列,消费者从队列中获取消息并消费。多个消费者可以订阅同一个队列,这时队列中的消息会被平均分摊(轮询)给多个消费者进行消费,而不是每个消费者都收到所有的消息进行消费。(注意:RabbitMQ不支持队列层面的广播消费,如果需要广播消费,可以采用一个交换器通过路由Key绑定多个队列,由多个消费者来订阅这些队列的方式。)Exchange:交换器。在RabbitMQ中,生产者并非直接将消息投递到队列中。真实情况是,生产者将消息发送到Exchange(交换器),由交换器将消息路由到一个或多个队列中。如果路由不到,或返回给生产者,或直接丢弃,或做其它处理。RoutingKey:路由Key。生产者将消息发送给交换器的时候,一般会指定一个RoutingKey,用来指定这个消息的路由规则。这个路由Key需要与交换器类型和绑定键(BindingKey)联合使用才能最终生效。在交换器类型和绑定键固定的情况下,生产者可以在发送消息给交换器时通过指定RoutingKey来决定消息流向哪里。Binding:RabbitMQ通过绑定将交换器和队列关联起来,在绑定的时候一般会指定一个绑定键,这样RabbitMQ就可以指定如何正确的路由到队列了。从这里我们可以看到在RabbitMQ中交换器和队列实际上可以是一对多,也可以是多对多关系。交换器和队列就像我们关系数据库中的两张表。他们同归BindingKey做关联(多对多关系表)。在我们投递消息时,可以通过Exchange和RoutingKey(对应BindingKey)就可以找到相对应的队列。RabbitMQ主要有四种类型的交换器:fanout:扇形交换器,它会把发送到该交换器的消息路由到所有与该交换器绑定的队列中。如果使用扇形交换器,则不会匹配路由Key。direct:direct交换器,会把消息路由到RoutingKey与BindingKey完全匹配的队列中。topic:完全匹配BindingKey和RoutingKey的direct交换器 有些时候并不能满足实际业务的需求。topic 类型的交换器在匹配规则上进行了扩展,它与direct 类型的交换器相似,也是将消息路由到BindingKey 和RoutingKey相匹配的队列中,但这里的匹配规则有些不同,它约定:1:RoutingKey 为一个点号".“分隔的字符串(被点号”.“分隔开的每一段独立的字符串称为一个单词),如"hs.rabbitmq.client”,“com.rabbit.client"等。2:BindingKey 和RoutingKey 一样也是点号”.“分隔的字符串;3:BindingKey 中可以存在两种特殊字符串"“和”#",用于做模糊匹配,其中"“用于匹配一个单词,”#“用于匹配多规格单词(可以是零个)。如图: · 路由键为” apple.rabbit.client” 的消息会同时路由到Queuel 和Queue2; · 路由键为" orange.mq.client" 的消息只会路由到Queue2 中: · 路由键为" apple.mq.demo" 的消息只会路由到Queue2 中: · 路由键为" banana.rabbit.demo" 的消息只会路由到Queuel 中: · 路由键为" orange.apple.banana" 的消息将会被丢弃或者返回给生产者因为它没有匹配任何路由键。header:headers 类型的交换器不依赖于路由键的匹配规则来路由消息,而是根据发送的消息内容中 的headers属性进行匹配。在绑定队列和交换器时制定一组键值对, 当发送消息到交换器时, RabbitMQ 会获取到该消息的headers(也是一个键值对的形式) ,对比其中的键值对是否完全 匹配队列和交换器绑定时指定的键值对,如果完全匹配则消息会路由到该队列,否则不会路由到该队列。(注:该交换器类型性能较差且不实用,因此一般不会用到)。了解了上面的概念,我们再来思考消息是如何从交换器到队列的。首先Rabbit在接收到消息时,会解析消息的标签从而得到消息的交换器与路由key信息。然后根据交换器的类型、路由key以及该交换器和队列的绑定关系来决定消息最终投递到哪个队列里面。三 RabbitMQ使用3.1 RabbitMQ安装这里我们基于docker来安装。3.1.1 拉取镜像docker pull rabbitmq:management3.1.2 启动容器docker run -d –name rabbit -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=admin -p 15672:15672 -p 5672:5672 -p 25672:25672 -p 61613:61613 -p 1883:1883 rabbitmq:management3.2 RabbitMQ 客户端开发使用这里我们以dotnet平台下RabbitMQ.Client3.6.9(可以从nuget中下载)为示例,简单介绍dotnet平台下对RabbitMQ的简单操作。更详细的内容可以从nuget中下载源码和文档进行查看。3.2.1 连接Rabbit ConnectionFactory factory = new ConnectionFactory(); factory.UserName = “admin”;//用户名 factory.Password = “admin”;//密码 factory.HostName = “192.168.17.205”;//主机名 factory.VirtualHost = “”;//虚拟主机(这个暂时不需要,稍后的文章里会介绍虚拟主机 的概念) factory.Port = 15672;//端口 IConnection conn = factory.CreateConnection();//创建连接 3.2.2 创建信道IModel channel = conn.CreateModel();说明:Connection 可以用来创建多个Channel 实例,但是Channel 实例不能在线程问共享,应用程序应该为每一个线程开辟一个Channel 。某些情况下Channel 的操作可以并发运行,但是在其他情况下会导致在网络上出现错误的通信帧交错,同时也会影响友送方确认( publisherconfrrm)机制的运行,所以多线程问共享Channel实例是非线程安全的。3.2.3 交换器、队列和绑定 channel.ExchangeDeclare(“exchangeName”, “direct”, true); String queueName = channel.QueueDeclare().QueueName; channel.QueueBind(queueName, “exchangeName”, “routingKey”);如上创建了一个持久化的、非自动删除的、绑定类型为direct 的交换器,同时也创建了一个非持久化的、排他的、自动删除的队列(此队列的名称由RabbitMQ 自动生成)。这里的交换器和队列也都没有设置特殊的参数。上面的代码也展示了如何使用路由键将队列和交换器绑定起来。上面声明的队列具备如下特性: 只对当前应用中同一个Connection 层面可用,同一个Connection 的不同Channel可共用,并且也会在应用连接断开时自动删除。上述方法根据参数不同,可以有不同的重载形式,根据自身的需要进行调用。ExchangeDeclare方法详解:ExchangeDeclare有多个重载方法,这些重载方法都是由下面这个方法中缺省的某些参数构成的。void ExchangeDeclare(string exchange, string type, bool durable, bool autoDelete, IDictionary<string, object> arguments);exchange : 交换器的名称。type : 交换器的类型,常见的如fanout、direct 、topicdurable: 设置是否持久化。durab l e 设置为true 表示持久化, 反之是非持久化。持久化可以将交换器存盘,在服务器重启的时候不会丢失相关信息。autoDelete : 设置是否自动删除。autoDelete 设置为true 则表示自动删除。自动删除的前提是至少有一个队列或者交换器与这个交换器绑定,之后所有与这个交换器绑定的队列或者交换器都与此解绑。注意不能错误地把这个参数理解为:“当与此交换器连接的客户端都断开时,RabbitMQ 会自动删除本交换器”。internal : 设置是否是内置的。如果设置为true,则表示是内置的交换器,客户端程序无法直接发送消息到这个交换器中,只能通过交换器路由到交换器这种方式。argument : 其他一些结构化参数,比如alternate - exchange。QueueDeclare方法详解:QueueDeclare只有两个重载。QueueDeclareOk QueueDeclare();QueueDeclareOk QueueDeclare(string queue, bool durable, bool exclusive, bool autoDelete, IDictionary<string, object> arguments);不带任何参数的queueDeclare 方法默认创建一个由RabbitMQ 命名的(类似这种amq.gen-LhQzlgv3GhDOv8PIDabOXA 名称,这种队列也称之为匿名队列〉、排他的、自动删除的、非持久化的队列。queue : 队列的名称。durable: 设置是否持久化。为true 则设置队列为持久化。持久化的队列会存盘,在服务器重启的时候可以保证不丢失相关信息。exclusive : 设置是否排他。为true 则设置队列为排他的。如果一个队列被声明为排他队列,该队列仅对首次声明它的连接可见,并在连接断开时自动删除。这里需要注意三点:排他队列是基于连接(Connection) 可见的,同一个连接的不同信道(Channel)是可以同时访问同一连接创建的排他队列;“首次"是指如果一个连接己经声明了一个排他队列,其他连接是不允许建立同名的排他队列的,这个与普通队列不同:即使该队列是持久化的,一旦连接关闭或者客户端退出,该排他队列都会被自动删除,这种队列适用于一个客户端同时发送和读取消息的应用场景。autoDelete: 设置是否自动删除。为true则设置队列为自动删除。自动删除的前提是:至少有一个消费者连接到这个队列,之后所有与这个队列连接的消费者都断开时,才会自动删除。不能把这个参数错误地理解为:当连接到此队列的所有客户端断开时,这个队列自动删除”,因为生产者客户端创建这个队列,或者没有消费者客户端与这个队列连接时,都不会自动删除这个队列。argurnents:设置队列的其他一些参数,如x-rnessage-ttl、x-expires、x-rnax-length、x-rnax-length-bytes、x-dead-letter-exchange、x-deadletter-routing-key,x-rnax-priority等。注意:生产者和消费者都能够使用queueDeclare来声明一个队列,但是如果消费者在同一个信道上订阅了另一个队列,就无法再声明队列了。必须先取消订阅,然后将信道直为"传输"模式,之后才能声明队列。在此我向大家推荐一个架构学习交流群。交流学习群号:478030634 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多QueueBind 方法详解:将队列和交换器绑定的方法如下:void QueueBind(string queue, string exchange, string routingKey, IDictionary<string, object> arguments);queue: 队列名称:exchange: 交换器的名称:routingKey: 用来绑定队列和交换器的路由键;argument: 定义绑定的一些参数。将队列与交换器解绑的方法如下: QueueUnbind(string queue, string exchange, string routingKey, IDictionary<string, object> arguments);其参数与绑定意义相同。注:除队列可以绑定交换器外,交换器同样可以绑定队列。即:ExchangeBind方法,其使用方式与队列绑定相似。3.2.4 发送消息发送消息可以使用BasicPublish方法。void BasicPublish(string exchange, string routingKey, bool mandatory,IBasicProperties basicProperties, byte[] body);exchange: 交换器的名称,指明消息需要发送到哪个交换器中。如果设置为空字符串,则消息会被发送到RabbitMQ 默认的交换器中。routingKey : 路由键,交换器根据路由键将消息存储到相应的队列之中。basicProperties: 消息的基本属性集。body : 消息体( pay1oad ),真正需要发送的消息。mandatory: 是否将消息返回给生产者(会在后续的文章中介绍这个参数).3.2.5 消费消息RabbitMQ 的消费模式分两种: 推(Push)模式和拉(Pull)模式。推模式采用BasicConsume进行消费,而拉模式则是调用BasicGet进行消费。推模式: EventingBasicConsumer consumer = new EventingBasicConsumer(channel);//定义消费者 对象consumer.Received += (model, ea) => { //do someting; channel.BasicAck(ea.DeliveryTag, multiple: false);//确认 }; channel.BasicConsume(queue: “queueName”, noAck: false, consumer: consumer);//订阅消息 string BasicConsume(string queue, bool noAck, string consumerTag, bool noLocal, bool exclusive, IDictionary<string, object> arguments, IBasicConsumer consumer);queue : 队列的名称:noAck : 设置是否需要确认,false为需要确认。consumerTag: 消费者标签,用来区分多个消费者:noLocal : 设置为true 则表示不能将同一个Connection中生产者发送的消息传送给这个Connection中的消费者:exclusive : 设置是否排他arguments : 设置消费者的其他参数consumer: 指定处理消息的消费者对象。拉模式BasicGetResult result = channel.BasicGet(“queueName”, noAck: false);//获取消息channel.BasicAck(result.DeliveryTag, multiple: false);//确认3.2.6 关闭连接在应用程序使用完之后,需要关闭连接,释放资源:channel.close();conn.close() ;显式地关闭Channel 是个好习惯,但这不是必须的,在Connection 关闭的时候,Channel 也会自动关闭。结束语以上简单介绍了分布式系统中消息中间件的概念与作用,以及RabbitMQ的一些基本概念与简单使用。下一篇文章将继续针对RabbitMQ进行总结。主要内容包括何时创建队列、RabbitMQ的确认机制、过期时间的使用、死信队列、以及利用RabbitMQ实现延迟队列……大家觉得文章对你还是有一点点帮助的,大家可以点击下方二维码进行关注。 《乐趣区》 公众号聊的不仅仅是Java技术知识,还有面试等干货,后期还有大量架构干货。大家一起关注吧!关注烂猪皮,你会了解的更多………….. 原文连接:https://www.cnblogs.com/hunte… ...

September 26, 2018 · 2 min · jiezi

良好的RPC接口设计,需要注意这些方面

RPC 框架的讨论一直是各个技术交流群中的热点话题,阿里的 dubbo,新浪微博的 motan,谷歌的 grpc,以及不久前蚂蚁金服开源的 sofa,都是比较出名的 RPC 框架。RPC 框架,或者一部分人习惯称之为服务治理框架,更多的讨论是存在于其技术架构,比如 RPC 的实现原理,RPC 各个分层的意义,具体 RPC 框架的源码分析…但却并没有太多话题和“如何设计 RPC 接口”这样的业务架构相关。可能很多小公司程序员还是比较关心这个问题的,这篇文章主要分享下一些个人眼中 RPC 接口设计的最佳实践。初识 RPC 接口设计由于 RPC 中的术语每个程序员的理解可能不同,所以文章开始,先统一下 RPC 术语,方便后续阐述。大家都知道共享接口是 RPC 最典型的一个特点,每个服务对外暴露自己的接口,该模块一般称之为 api;外部模块想要实现对该模块的远程调用,则需要依赖其 api;每个服务都需要有一个应用来负责实现自己的 api,一般体现为一个独立的进程,该模块一般称之为 app。api 和 app 是构建微服务项目的最简单组成部分,如果使用 maven 的多 module 组织代码,则体现为如下的形式。serviceA 服务serviceA/pom.xml 定义父 pom 文件 <modules> <module>serviceA-api</module> <module>serviceA-app</module></modules><packaging>pom</packaging><groupId>moe.cnkirito</groupId><artifactId>serviceA</artifactId><version>1.0.0-SNAPSHOT</version>serviceA/serviceA-api/pom.xml 定义对外暴露的接口,最终会被打成 jar 包供外部服务依赖 <parent> <artifactId>serviceA</artifactId> <groupId>moe.cnkirito</groupId> <version>1.0.0-SNAPSHOT</version></parent><packaging>jar</packaging><artifactId>serviceA-api</artifactId>serviceA/serviceA-app/pom.xml 定义了服务的实现,一般是 springboot 应用,所以下面的配置文件中,我配置了 springboot 应用打包的插件,最终会被打成 jar 包,作为独立的进程运行。 <parent> <artifactId>serviceA</artifactId> <groupId>moe.cnkirito</groupId> <version>1.0.0-SNAPSHOT</version> </parent> <packaging>jar</packaging> <artifactId>serviceA-app</artifactId> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>麻雀虽小,五脏俱全,这样一个微服务模块就实现了。旧 RPC 接口的痛点统一好术语,这一节来描述下我曾经遭遇过的 RPC 接口设计的痛点,相信不少人有过相同的遭遇。查询接口过多各种 findBy 方法,加上各自的重载,几乎占据了一个接口 80% 的代码量。这也符合一般人的开发习惯,因为页面需要各式各样的数据格式,加上查询条件差异很大,便造成了:一个查询条件,一个方法的尴尬场景。这样会导致另外一个问题,需要使用某个查询方法时,直接新增了方法,但实际上可能这个方法已经出现过了,隐藏在了令人眼花缭乱的方法中。难以扩展接口的任何改动,比如新增一个入参,都会导致调用者被迫升级,这也通常是 RPC 设计被诟病的一点,不合理的 RPC 接口设计会放大这个缺点。升级困难在之前的 “初识 RPC 接口设计”一节中,版本管理的粒度是 project,而不是 module,这意味着:api 即使没有发生变化,app 版本演进,也会造成 api 的被迫升级,因为 project 是一个整体。问题又和上一条一样了,api 一旦发生变化,调用者也得被迫升级,牵一发而动全身。难以测试接口一多,职责随之变得繁杂,业务场景各异,测试用例难以维护。特别是对于那些有良好习惯编写单元测试的程序员而言,简直是噩梦,用例也得跟着改。异常设计不合理在既往的工作经历中曾经有一次会议,就 RPC 调用中的异常设计引发了争议,一派人觉得需要有一个业务 CommonResponse,封装异常,每次调用后,优先判断调用结果是否 success,在进行业务逻辑处理;另一派人觉得这比较麻烦,由于 RPC 框架是可以封装异常调用的,所以应当直接 try catch 异常,不需要进行业务包裹。在没有明确规范时,这两种风格的代码同时存在于项目中,十分难看!单参数接口如果你使用过 springcloud ,可能会不适应 http 通信的限制,因为 @RequestBody 只能使用单一的参数,也就意味着,springcloud 构建的微服务架构下,接口天然是单参数的。而 RPC 方法入参的个数在语法层面是不会受到限制的,但如果强制要求入参为单参数,会解决一部分的痛点。使用 Specification 模式解决查询接口过多的问题public interface StudentApi{Student findByName(String name);List<Student> findAllByName(String name);Student findByNameAndNo(String name,String no);Student findByIdcard(String Idcard);}如上的多个查询方法目的都是同一个:根据条件查询出 Student,只不过查询条件有所差异。试想一下,Student 对象假设有 10 个属性,最坏的情况下它们的排列组合都可能作为查询条件,这便是查询接口过多的根源。public interface StudentApi{Student findBySpec(StudentSpec spec);List<Student> findListBySpec(StudentListSpec spec);Page<Student> findPageBySpec(StudentPageSpec spec);}上述接口便是最通用的单参接口,三个方法几乎囊括了 99% 的查询条件。所有的查询条件都被封装在了 StudentSpec,StudentListSpec,StudentPageSpec 之中,分别满足了单对象查询,批量查询,分页查询的需求。如果你了解领域驱动设计,会发现这里借鉴了其中 Specification 模式的思想。单参数易于做统一管理 public interface SomeProvider {void opA(ARequest request);void opB(BRequest request);CommonResponse<C> opC(CRequest request); }入参中的入参虽然形态各异,但由于是单个入参,所以可以统一继承 AbstractBaseRequest,即上述的 ARequest,BRequest,CRequest 都是 AbstractBaseRequest 的子类。在千米内部项目中,AbstractBaseRequest 定义了 traceId、clientIp、clientType、operationType 等公共入参,减少了重复命名,我们一致认为,这更加的 OO。有了 AbstractBaseRequest,我们可以更加轻松地在其之上做 AOP,千米的实践中,大概做了如下的操作:请求入参统一校验(request.checkParam(); param.checkParam();)实体变更统一加锁,降低锁粒度请求分类统一处理(if (request instanceof XxxRequest))请求报文统一记日志(log.setRequest(JsonUtil.getJsonString(request)))操作成功统一发消息如果不遵守单参数的约定,上述这些功能也并不是无法实现,但所需花费的精力远大于单参数,一个简单的约定带来的优势,我们认为是值得的。单参数入参兼容性强还记得前面的小节中,我提到了 SpringCloud,在 SpringCloud Feign 中,接口的入参通常会被 @RequestBody 修饰,强制做单参数的限制。千米内部使用了 Dubbo 作为 Rpc 框架,一般而言,为 Dubbo 服务设计的接口是不能直接用作 Feign 接口的(主要是因为 @RequestBody 的限制),但有了单参数的限制,便使之成为了可能。为什么我好端端的 Dubbo 接口需要兼容 Feign 接口?可能会有人发出这样的疑问,莫急,这样做的初衷当然不是为了单纯做接口兼容,而是想充分利用 HTTP 丰富的技术栈以及一些自动化工具。自动生成 HTTP 接口实现(让服务端同时支持 Dubbo 和 HTTP 两种服务接口)看过我之前文章的朋友应该了解过一个设计:千米内部支持的是 Dubbo 协议和 HTTP 协议族(如 JSON RPC 协议,Restful 协议),这并不意味着程序员需要写两份代码,我们可以通过 Dubbo 接口自动生成 HTTP 接口,体现了单参数设计的兼容性之强。通过 Swagger UI 实现对 Dubbo 接口的可视化便捷测试又是一个兼容 HTTP 技术栈带来的便利,在 Restful 接口的测试中,Swagger 一直是备受青睐的一个工具,但可惜的是其无法对 Dubbo 接口进行测试。兼容 HTTP 后,我们只需要做一些微小的工作,便可以实现 Swagger 对 Dubbo 接口的可视化测试。有利于 TestNg 集成测试自动生成 TestNG 集成测试代码和缺省测试用例,这使得服务端接口集成测试变得异常简单,程序员更能集中精力设计业务用例,结合缺省用例、JPA 自动建表和 PowerMock 模拟外部依赖接口实现本机环境。这块涉及到了公司内部的代码,只做下简单介绍,我们一般通过内部项目 com.qianmi.codegenerator:api-dubbo-2-restful ,com.qianmi.codegenerator:api-request-json 生成自动化的测试用例,方便测试。而这些自动化工具中大量使用了反射,而由于单参数的设计,反射用起来比较方便。接口异常设计首先肯定一点,RPC 框架是可以封装异常的,Exception 也是返回值的一部分。在 go 语言中可能更习惯于返回 err,res 的组合,但 JAVA 中我个人更偏向于 try catch 的方法捕获异常。RPC 接口设计中的异常设计也是一个注意点。初始方案 public interface ModuleAProvider { void opA(ARequest request); void opB(BRequest request); CommonResponse<C> opC(CRequest request); }我们假设模块 A 存在上述的 ModuleAProvider 接口,ModuleAProvider 的实现中或多或少都会出现异常,例如可能存在的异常 ModuleAException,调用者实际上并不知道 ModuleAException 的存在,只有当出现异常时,才会知晓。对于 ModuleAException 这种业务异常,我们更希望调用方能够显示的处理,所以 ModuleAException 应该被设计成 Checked Excepition。正确的异常设计姿势public interface ModuleAProvider {void opA(ARequest request) throws ModuleAException;void opB(BRequest request) throws ModuleAException;CommonResponse<C> opC(CRequest request) throws ModuleAException;}上述接口中定义的异常实际上也是一种契约,契约的好处便是不需要叙述,调用方自然会想到要去处理 Checked Exception,否则连编译都过不了。调用方的处理方式在 ModuleB 中,应当如下处理异常: public class ModuleBService implements ModuleBProvider {@ReferenceModuleAProvider moduleAProvider;@Overridepublic void someOp() throws ModuleBexception{ try{ moduleAProvider.opA(…); }catch(ModuleAException e){ throw new ModuleBException(e.getMessage()); }}@Overridepublic void anotherOp(){ try{ moduleAProvider.opB(…); }catch(ModuleAException e){ // 业务逻辑处理 }}}someOp 演示了一个异常流的传递,ModuleB 暴露出去的异常应当是 ModuleB 的 api 模块中异常类,虽然其依赖了 ModuleA ,但需要将异常进行转换,或者对于那些意料之中的业务异常可以像 anotherOp() 一样进行处理,不再传递。这时如果新增 ModuleC 依赖 ModuleB,那么 ModuleC 完全不需要关心 ModuleA 的异常。异常与熔断作为系统设计者,我们应该认识到一点: RPC 调用,失败是常态。通常我们需要对 RPC 接口做熔断处理,比如千米内部便集成了 Netflix 提供的熔断组件 Hystrix。Hystrix 需要知道什么样的异常需要进行熔断,什么样的异常不能够进行熔断。在没有上述的异常设计之前,回答这个问题可能还有些难度,但有了 Checked Exception 的契约,一切都变得明了清晰了。public class ModuleAProviderProxy {@Referenceprivate ModuleAProvider moduleAProvider;@HystrixCommand(ignoreExceptions = {ModuleAException.class})public void opA(ARequest request) throws ModuleAException { moduleAProvider.opA(request);}@HystrixCommand(ignoreExceptions = {ModuleAException.class})public void opB(BRequest request) throws ModuleAException { moduleAProvider.oBB(request);}@HystrixCommand(ignoreExceptions = {ModuleAException.class})public CommonResponse<C> opC(CRequest request) throws ModuleAException { return moduleAProvider.opC(request);}}如服务不可用等原因引发的多次接口调用超时异常,会触发 Hystrix 的熔断;而对于业务异常,我们则认为不需要进行熔断,因为对于接口 throws 出的业务异常,我们也认为是正常响应的一部分,只不过借助于 JAVA 的异常机制来表达。实际上,和生成自动化测试类的工具一样,我们使用了另一套自动化的工具,可以由 Dubbo 接口自动生成对应的 Hystrix Proxy。我们坚定的认为开发体验和用户体验一样重要,所以公司内部会有非常多的自动化工具。API 版本单独演进引用一段公司内部的真实对话:A:我下载了你们的代码库怎么编译不通过啊,依赖中 xxx-api-1.1.3 版本的 jar 包找不到了,那可都是 RELEASE 版本啊。B:你不知道我们 nexus 容量有限,只能保存最新的 20 个 RELEASE 版本吗?那个 API 现在最新的版本是 1.1.31 啦。A:啊,这才几个月就几十个 RELEASE 版本啦?这接口太不稳定啦。B: 其实接口一行代码没改,我们业务分析是很牛逼的,一直很稳定。但是这个 API是和我们项目一起打包的,我们需求更新一次,就发布一次,API 就被迫一起升级版本。发生这种事,大家都不想的。在单体式架构中,版本演进的单位是整个项目。微服务解决的一个关键的痛点便是其做到了每个服务的单独演进,这大大降低了服务间的耦合。正如我文章开始时举得那个例子一样:serviceA 是一个演进的单位,serviceA-api 和 serviceA-app 这两个 Module 从属于 serviceA,这意味着 app 的一次升级,将会引发 api 的升级,因为他们是共生的!而从微服务的使用角度来看,调用者关心的是 api 的结构,而对其实现压根不在乎。所以对于 api 定义未发生变化,其 app 发生变化的那些升级,其实可以做到对调用者无感知。在实践中也是如此api 版本的演进应该是缓慢的,而 app 版本的演进应该是频繁的。所以,对于这两个演进速度不一致的模块,我们应该单独做版本管理,他们有自己的版本号。问题回归查询接口过多各种 findBy 方法,加上各自的重载,几乎占据了一个接口 80% 的代码量。这也符合一般人的开发习惯,因为页面需要各式各样的数据格式,加上查询条件差异很大,便造成了:一个查询条件,一个方法的尴尬场景。这样会导致另外一个问题,需要使用某个查询方法时,直接新增了方法,但实际上可能这个方法已经出现过了,隐藏在了令人眼花缭乱的方法中。解决方案:使用单参+Specification 模式,降低重复的查询方法,大大降低接口中的方法数量。难以扩展接口的任何改动,比如新增一个入参,都会导致调用者被迫升级,这也通常是 RPC 设计被诟病的一点,不合理的 RPC 接口设计会放大这个缺点。解决方案:单参设计其实无形中包含了所有的查询条件的排列组合,可以直接在 app 实现逻辑的新增,而不需要对 api 进行改动(如果是参数的新增则必须进行 api 的升级,参数的废弃可以用 @Deprecated 标准)。升级困难在之前的 “初识 RPC 接口设计”一节中,版本管理的粒度是 project,而不是 module,这意味着:api 即使没有发生变化,app 版本演进,也会造成 api 的被迫升级,因为 project 是一个整体。问题又和上一条一样了,api 一旦发生变化,调用者也得被迫升级,牵一发而动全身。解决方案:以 module 为版本演进的粒度。api 和 app 单独演进,减少调用者的不必要升级次数。难以测试接口一多,职责随之变得繁杂,业务场景各异,测试用例难以维护。特别是对于那些有良好习惯编写单元测试的程序员而言,简直是噩梦,用例也得跟着改。解决方案:单参数设计+自动化测试工具,打造良好的开发体验。异常设计不合理在既往的工作经历中曾经有一次会议,就 RPC 调用中的异常设计引发了争议,一派人觉得需要有一个业务 CommonResponse,封装异常,每次调用后,优先判断调用结果是否 success,在进行业务逻辑处理;另一派人觉得这比较麻烦,由于 RPC 框架是可以封装异常调用的,所以应当直接 try catch 异常,不需要进行业务包裹。在没有明确规范时,这两种风格的代码同时存在于项目中,十分难看!解决方案:Checked Exception+正确异常处理姿势,使得代码更加优雅,降低了调用方不处理异常带来的风险。原文出处:https://www.jianshu.com/p/dca…作者:占小狼 ...

September 24, 2018 · 3 min · jiezi

分库分表后如何部署上线?

引言我们先来讲一个段子面试官:“有并发的经验没?”应聘者:“有一点。”面试官:“那你们为了处理并发,做了哪些优化?”应聘者:“前后端分离啊,限流啊,分库分表啊。。”面试官:“谈谈分库分表吧?“应聘者:“bala。bala。bala。。”面试官心理活动:这个仁兄讲的怎么这么像网上的博客抄的,容我再问问。面试官:“你们分库分表后,如何部署上线的?”应聘者:“这!!!!!!”不要惊讶,写这篇文章前,我特意去网上看了下分库分表的文章,很神奇的是,都在讲怎么进行分库分表,却不说分完以后,怎么部署上线的。这样在面试的时候就比较尴尬了。你们自己摸着良心想一下,如果你真的做过分库分表,你会不知道如何部署的么?因此我们来学习一下如何部署吧。ps: 我发现一个很神奇的现象。因为很多公司用的技术比较low,那么一些求职者为了提高自己的竞争力,就会将一些高大上的技术写进自己的low项目中。然后呢,他出去面试害怕碰到从这个公司出来的人,毕竟从这个公司出来的人,一定知道自己以前公司的项目情形。因此为了圆谎,他就会说:“他们从事的是这个公司的老项目改造工作,用了很多新技术进去!”那么,请你好好思考一下,你们的老系统是如何平滑升级为新系统的!如何部署停机部署法大致思路就是,挂一个公告,半夜停机升级,然后半夜把服务停了,跑数据迁移程序,进行数据迁移。步骤如下:(1)出一个公告,比如“今晚00:00~6:00进行停机维护,暂停服务”(2)写一个迁移程序,读 db-old 数据库,通过中间件写入新库 db-new1 和 db-new2 ,具体如下图所示(3)校验迁移前后一致性,没问题就切该部分业务到新库。顺便科普一下,这个中间件。现在流行的分库分表的中间件有两种,一种是 proxy 形式的,例如 mycat ,是需要额外部署一台服务器的。还有一种是 client 形式的,例如当当出的 Sharding-JDBC ,就是一个jar包,使用起来十分轻便。我个人偏向 Sharding-JDBC ,这种方式,无需额外部署,无其他依赖,DBA也无需改变原有的运维方式。评价:大家不要觉得这种方法low,我其实一直觉得这种方法可靠性很强。而且我相信各位读者所在的公司一定不是什么很牛逼的互联网公司,如果你们的产品凌晨1点的用户活跃数还有超过1000的,你们握个爪!毕竟不是所有人都在什么电商公司的,大部分产品半夜都没啥流量。所以此方案,并非没有可取之处。但是此方案有一个缺点, 累! 不止身体累,心也累!你想想看,本来定六点结束,你五点把数据库迁移好,但是不知怎么滴,程序切新库就是有点问题。于是,眼瞅着天就要亮了,赶紧把数据库切回老库。第二个晚上继续这么干,简直是身心俱疲。ps: 这里教大家一些技巧啊,如果你真的没做过分库分表,又想吹一波,涨一下工资,建议答这个方案。因为这个方案比较low,low到没什么东西可以深挖的,所以答这个方案,比较靠谱。另外,如果面试官的问题是你们怎么进行分库分表的?这个问题问的很泛,所以回答这个问题建议自己主动把分表的策略,以及如何部署的方法讲出来。因为这么答,显得严谨一些。不过,很多面试官为了卖弄自己的技术,喜欢这么问分表有哪些策略啊?你们用哪种啊?ok。。这个问题具体指向了分库分表的某个方向了,你不要主动答如何进行部署的。等面试官问你,你再答。如果面试官没问,在面试最后一个环节,面试官会让你问让几个问题。你就问你刚才刚好有提到分库分表的相关问题,我们当时部署的时候,先停机。然后半夜迁移数据,然后第二天将流量切到新库,这种方案太累,不知道贵公司有没有什么更好的方案?那么这种情况下,面试官会有两种回答。第一种,面试官硬着头皮随便扯。第二种,面试官真的做过,据实回答。记住,面试官怎么回答的不重要。重点的是,你这个问题出去,会给面试官一种错觉:“这个小伙子真的做过分库分表。“如果你担心进去了,真派你去做分库分表怎么办?OK,不要怕。我赌你试用期碰不到这个活。因为能进行分库分表,必定对业务非常熟。还在试用期的你,必定对业务不熟,如果领导给你这种活,我只能说他有一颗大心脏。ok,指点到这里。面试本来就是一场斗智斗勇的过程,扯远了,回到我们的主题。双写部署法(一)这个就是不停机部署法,这里我需要先引进两个概念: 历史数据 和 增量数据 。假设,我们是对一张叫做 test_tb 的表进行拆分,因为你要进行双写,系统里头和 test_tb表有关的业务之前必定会加入一段双写代码,同时往老库和新库中写,然后进行部署,那么历史数据:在该次部署前,数据库表 test_tb 的有关数据,我们称之为历史数据。增量数据:在该次部署后,数据库表 test_tb 的新产生的数据,我们称之为增量数据。然后迁移流程如下(1)先计算你要迁移的那张表的 max(主键) 。在迁移过程中,只迁移 db-old 中 test_tb 表里,主键小等于该 max(主键) 的值,也就是所谓的历史数据。这里有特殊情况,如果你的表用的是uuid,没法求出 max(主键) ,那就以创建时间作为划分历史数据和增量数据的依据。如果你的表用的是uuid,又没有创建时间这个字段,我相信机智的你,一定有办法区分出历史数据和增量数据。(2)在代码中,与 test_tb 有关的业务,多加一条往消息队列中发消息的代码,将操作的sql发送到消息队列中,至于消息体如何组装,大家自行考虑。 需要注意的是, 只发写请求的sql,只发写请求的sql,只发写请求的sql。重要的事情说三遍!原因有二:(1)只有写请求的sql对恢复数据才有用。(2)系统中,绝大部分的业务需求是读请求,写请求比较少。注意了,在这个阶段,我们不消费消息队列里的数据。我们只发写请求,消息队列的消息堆积情况不会太严重!(3)系统上线。另外,写一段迁移程序,迁移 db-old 中 test_tb 表里,主键小于该 max(主键)的数据,也就是所谓的历史数据。上面步骤(1)~步骤(3)的过程如下(3)校验迁移前后一致性,没问题就切该部分业务到新库。顺便科普一下,这个中间件。现在流行的分库分表的中间件有两种,一种是 proxy 形式的,例如 mycat ,是需要额外部署一台服务器的。还有一种是 client 形式的,例如当当出的 Sharding-JDBC ,就是一个jar包,使用起来十分轻便。我个人偏向 Sharding-JDBC ,这种方式,无需额外部署,无其他依赖,DBA也无需改变原有的运维方式。评价:大家不要觉得这种方法low,我其实一直觉得这种方法可靠性很强。而且我相信各位读者所在的公司一定不是什么很牛逼的互联网公司,如果你们的产品凌晨1点的用户活跃数还有超过1000的,你们握个爪!毕竟不是所有人都在什么电商公司的,大部分产品半夜都没啥流量。所以此方案,并非没有可取之处。但是此方案有一个缺点, 累! 不止身体累,心也累!你想想看,本来定六点结束,你五点把数据库迁移好,但是不知怎么滴,程序切新库就是有点问题。于是,眼瞅着天就要亮了,赶紧把数据库切回老库。第二个晚上继续这么干,简直是身心俱疲。ps: 这里教大家一些技巧啊,如果你真的没做过分库分表,又想吹一波,涨一下工资,建议答这个方案。因为这个方案比较low,low到没什么东西可以深挖的,所以答这个方案,比较靠谱。另外,如果面试官的问题是你们怎么进行分库分表的?这个问题问的很泛,所以回答这个问题建议自己主动把分表的策略,以及如何部署的方法讲出来。因为这么答,显得严谨一些。不过,很多面试官为了卖弄自己的技术,喜欢这么问分表有哪些策略啊?你们用哪种啊?ok。。这个问题具体指向了分库分表的某个方向了,你不要主动答如何进行部署的。等面试官问你,你再答。如果面试官没问,在面试最后一个环节,面试官会让你问让几个问题。你就问你刚才刚好有提到分库分表的相关问题,我们当时部署的时候,先停机。然后半夜迁移数据,然后第二天将流量切到新库,这种方案太累,不知道贵公司有没有什么更好的方案?那么这种情况下,面试官会有两种回答。第一种,面试官硬着头皮随便扯。第二种,面试官真的做过,据实回答。记住,面试官怎么回答的不重要。重点的是,你这个问题出去,会给面试官一种错觉:“这个小伙子真的做过分库分表。“如果你担心进去了,真派你去做分库分表怎么办?OK,不要怕。我赌你试用期碰不到这个活。因为能进行分库分表,必定对业务非常熟。还在试用期的你,必定对业务不熟,如果领导给你这种活,我只能说他有一颗大心脏。ok,指点到这里。面试本来就是一场斗智斗勇的过程,扯远了,回到我们的主题。双写部署法(一)这个就是不停机部署法,这里我需要先引进两个概念: 历史数据 和 增量数据 。假设,我们是对一张叫做 test_tb 的表进行拆分,因为你要进行双写,系统里头和 test_tb表有关的业务之前必定会加入一段双写代码,同时往老库和新库中写,然后进行部署,那么历史数据:在该次部署前,数据库表 test_tb 的有关数据,我们称之为历史数据。增量数据:在该次部署后,数据库表 test_tb 的新产生的数据,我们称之为增量数据。然后迁移流程如下(1)先计算你要迁移的那张表的 max(主键) 。在迁移过程中,只迁移 db-old 中 test_tb 表里,主键小等于该 max(主键) 的值,也就是所谓的历史数据。这里有特殊情况,如果你的表用的是uuid,没法求出 max(主键) ,那就以创建时间作为划分历史数据和增量数据的依据。如果你的表用的是uuid,又没有创建时间这个字段,我相信机智的你,一定有办法区分出历史数据和增量数据。(2)在代码中,与 test_tb 有关的业务,多加一条往消息队列中发消息的代码,将操作的sql发送到消息队列中,至于消息体如何组装,大家自行考虑。 需要注意的是, 只发写请求的sql,只发写请求的sql,只发写请求的sql。重要的事情说三遍!原因有二:(1)只有写请求的sql对恢复数据才有用。(2)系统中,绝大部分的业务需求是读请求,写请求比较少。注意了,在这个阶段,我们不消费消息队列里的数据。我们只发写请求,消息队列的消息堆积情况不会太严重!(3)系统上线。另外,写一段迁移程序,迁移 db-old 中 test_tb 表里,主键小于该 max(主键)的数据,也就是所谓的历史数据。上面步骤(1)~步骤(3)的过程如下等到 db-old 中的历史数据迁移完毕,则开始迁移增量数据,也就是在消息队列里的数据。(4)将迁移程序下线,写一段订阅程序订阅消息队列中的数据(5)订阅程序将订阅到到数据,通过中间件写入新库(6)新老库一致性验证,去除代码中的双写代码,将涉及到 test_tb 表的读写操作,指向新库。上面步骤(4)~步骤(6)的过程如下这里大家可能会有一个问题,在步骤(1)~步骤(3),系统对历史数据进行操作,会造成不一致的问题么?OK,不会。这里我们对 delete 操作和 update 操作做分析,因为只有这两个操作才会造成历史数据变动, insert 进去的数据都是属于增量数据。(1)对 db-old 的 test_tb 表的历史数据发出 delete 操作,数据还未删除,就被迁移程序给迁走了。此时 delete 操作在消息队列里还有记录,后期订阅程序订阅到该 delete 操作,可以进行删除。(2)对 db-old 的 test_tb 表的历史数据发出 delete 操作,数据已经删除,迁移程序迁不走该行数据。此时 delete 操作在消息队列里还有记录,后期订阅程序订阅到该 delete 操作,再执行一次 delete ,并不会对一致性有影响。对 update 的操作类似,不赘述。双写部署法(二)上面的方法有一个硬伤,注意我有一句话(2)在代码中,与test_tb有关的业务,多加一条往消息队列中发消息的代码,将操作的sql发送到消息队列中,至于消息体如何组装,大家自行考虑。大家想一下,这么做,是不是造成了严重的代码入侵。将非业务代码嵌入业务代码,这么做,后期删代码的时候特别累。有没什么方法,可以避免这个问题的?有的,订阅 binlog 日志。关于 binlog 日志,我尽量下周写一篇《研发应该掌握的binlog知识》,这边我就介绍一下作用记录所有数据库表结构变更(例如CREATE、ALTER TABLE…)以及表数据修改(INSERT、UPDATE、DELETE…)的二进制日志。binlog不会记录SELECT和SHOW这类操作,因为这类操作对据本身并没有修改。还记得我们在 双写部署法(一) 里介绍的,往消息队列里发的消息,都是写操作的消息。而 binlog 日志记录的也是写操作。所以订阅该日志,也能满足我们的需求。于是步骤如下(1)打开binlog日志,系统正常上线就好(2)还是写一个迁移程序,迁移历史数据。步骤和上面类似,不啰嗦了。步骤(1)~步骤(2)流程图如下(3)写一个订阅程序,订阅binlog(mysql中有 canal 。至于oracle中,大家就随缘自己写吧)。然后将订阅到到数据通过中间件,写入新库。(4)检验一致性,没问题就切库。步骤(3)~步骤(4)流程图如下怎么验数据一致性这里大概介绍一下吧,这篇的篇幅太长了,大家心里有底就行。(1)先验数量是否一致,因为验数量比较快。至于验具体的字段,有两种方法:(2.1)有一种方法是,只验关键性的几个字段是否一致。(2.2)还有一种是 ,一次取50条(不一定50条,具体自己定,我只是举例),然后像拼字符串一样,拼在一起。用md5进行加密,得到一串数值。新库一样如法炮制,也得到一串数值,比较两串数值是否一致。如果一致,继续比较下50条数据。如果发现不一致,用二分法确定不一致的数据在0-25条,还是26条-50条。以此类推,找出不一致的数据,进行记录即可。合理利用自己每一分每一秒的时间来学习提升自己,不要再用"没有时间“来掩饰自己思想上的懒惰!趁年轻,使劲拼,给未来的自己一个交代! ...

September 20, 2018 · 1 min · jiezi