使用virtualenv或pyenv构建python虚拟环境

有时候在安装python环境时会遇到一些奇葩的问题,比如有些包无论如何也安装不了,受限于python版本,有些环境部署实在麻烦。因此我建议使用虚拟环境来部署python,比如一个项目就单独建立一个python虚拟环境,与其他项目互不干扰。python虚拟环境工具很多,这里主要介绍virtualenv与pyenv。

virtualenv

virtualenv是跨平台的,linux、mac、windows都可以使用。

install

pip install virtualenv

创建虚拟目录

virtualenv kvenv -p /usr/bin/python2

说明:创建完成后会生成一个kvenv目录,可以加上-p参数指定Python版本。(当然要系统安装了某版本的python才能创建这个版本的虚拟目录)

激活虚拟环境

source kvenv/bin/activate

退出虚拟环境

deactivate

查看python路径

which python # 看python路径是否为新创建的虚拟目录

说明:Mac、linux与windows上安装使用方法一样。

pyenv

pyenv严格来说是python的版本控制器,使用很灵活。

Install

$ brew update
$ brew install peen

配置环境变量

$ echo 'eval "$(pyenv init -)"' >> ~/.bash_profile
source ~/.bash_profile

Usage

  • pyenv version # 当前版本
  • pyenv versions # 所有版本
  • pyenv global system # 全局切换
  • pyenv local 2.7.10 # 本地切换
  • pyenv local 3.5.0 –unset # 取消切换

pyenv常用命令

$ pyenv install --list #列出可安装版本
$ pyenv install <version> # 安装对应版本
$ pyenv versions # 显示当前使用的python版本
$ pyenv which python # 显示当前python安装路径
$ pyenv global <version> # 设置默认Python版本
$ pyenv local <version> # 当前路径创建一个.python-version, 以后进入这个目录自动切换为该版本
$ pyenv shell <version> # 当前shell的session中启用某版本,优先级高于global 及 local

安装其他版本python

pyenv install xx.xx.xx (pyenv install 3.4.3) #安装python3.4.3
pyenv rehash   # 安装完以后记得一定要rehash

virtualenv or pyenv ?

如果是项目环境,建议virtualenv,环境独立,也不会有很大的Bug。

如果只是个人学习练习python,可以使用pyenv,切换方便。

通过python和websocket构建实时通信系统[扩展saltstack监控]

先放一个小demo~

用html5的websocket实现的聊天平台。后端用的是python bottle框架。
后期要改成监控,可能要联合saltstack做实时的监控。
像上篇博客说的那样,实时监控就那点东西,就是接收数据、显示数据 。

像下面这样:

未分类

WebSocket API是下一代客户端-服务器的异步通信方法。该通信取代了单个的TCP套接字,使用ws或wss协议,可用于任意的客户端和服务器程序。WebSocket目前由W3C进行标准化。WebSocket已经受到Firefox 4、Chrome 、Opera 10.70以及Safari 5等浏览器的支持。

WebSocket API最伟大之处在于服务器和客户端可以在给定的时间范围内的任意时刻,相互推送信息。WebSocket并不限于以Ajax(或XHR)方式通信,因为Ajax技术需要客户端发起请求,而WebSocket服务器和客户端可以彼此相互推送信息;XHR受到域的限制,而WebSocket允许跨域通信。

WebSocket的优点

a)、服务器与客户端之间交换的标头信息很小,大概只有2字节;

b)、客户端与服务器都可以主动传送数据给对方;

c)、不用频率创建TCP请求及销毁请求,减少网络带宽资源的占用,同时也节省服务器资源;

建立连接的握手

当Web应用程序调用new WebSocket(url)接口时,Browser就开始了与地址为url的WebServer建立握手连接的过程。

  1. Browser与WebSocket服务器通过TCP三次握手建立连接,如果这个建立连接失败,那么后面的过程就不会执行,Web应用程序将收到错误消息通知。

  2. 在TCP建立连接成功后,Browser/UA通过http协议传送WebSocket支持的版本号,协议的字版本号,原始地址,主机地址等等一些列字段给服务器端。

  3. WebSocket服务器收到Browser/UA发送来的握手请求后,如果数据包数据和格式正确,客户端和服务器端的协议版本号匹配等等,就接受本次握手连接,并给出相应的数据回复,同样回复的数据包也是采用http协议传输。

  4. Browser收到服务器回复的数据包后,如果数据包内容、格式都没有问题的话,就表示本次连接成功,触发onopen消息,此时Web开发者就可以在此时通过send接口想服务器发送数据。否则,握手连接失败,Web应用程序会收到onerror消息,并且能知道连接失败的原因。
    这个握手很像HTTP,但是实际上却不是,它允许服务器以HTTP的方式解释一部分handshake的请求,然后切换为websocket
    数据传输
    WebScoket协议中,数据以帧序列的形式传输。

考虑到数据安全性,客户端向服务器传输的数据帧必须进行掩码处理。服务器若接收到未经过掩码处理的数据帧,则必须主动关闭连接。

服务器向客户端传输的数据帧一定不能进行掩码处理。客户端若接收到经过掩码处理的数据帧,则必须主动关闭连接。

针对上情况,发现错误的一方可向对方发送close帧(状态码是1002,表示协议错误),以关闭连接。

未分类

ws的连接状态:

GET /chat HTTP/1.1
Upgrade: WebSocket
Connection: Upgrade
Host: 66.xiaorui.cc:10000
Origin: http://66.xiaorui.cc
Cookie: somenterCookie

简单了解下接口方法和属性:

readyState表示连接有四种状态:

  • CONNECTING (0):表示还没建立连接;
  • OPEN (1): 已经建立连接,可以进行通讯;
  • CLOSING (2):通过关闭握手,正在关闭连接;
  • CLOSED (3):连接已经关闭或无法打开;

  • url是代表 WebSocket 服务器的网络地址,协议通常是”ws”或“wss(加密通信)”,send 方法就是发送数据到服务器端;

  • close 方法就是关闭连接;
  • onopen连接建立,即握手成功触发的事件;
  • onmessage收到服务器消息时触发的事件;
  • onerror异常触发的事件;
  • onclose关闭连接触发的事件;

来个例子,咱们用js来搞搞

var wsServer = 'ws://localhost:8888/Demo'; //服务器地址
var websocket = new WebSocket(wsServer); //创建WebSocket对象
websocket.send("hello");//向服务器发送消息
alert(websocket.readyState);//查看websocket当前状态
websocket.onopen = function (evt) {
    //已经建立连接
};
websocket.onclose = function (evt) {
    //已经关闭连接
};
websocket.onmessage = function (evt) {
    //收到服务器消息,使用evt.data提取
};
websocket.onerror = function (evt) {
    //产生异常
};

我的后端代码:

python的后端实现websocket的处理,有很多方法的。

比较常见的是 gevent的websocket的方式。

from bottle import get, run, template
from bottle.ext.websocket import GeventWebSocketServer
from bottle.ext.websocket import websocket
import gevent
users = set()
@get('/')
def index():
    return template('index')
@get('/websocket', apply=[websocket])
def chat(ws):
    users.add(ws)
    while True:
        msg = ws.receive()
        if msg is not None:
            for u in users:
                print type(u)
                u.send(msg)
                print u,msg
        else: break
    users.remove(ws)
run(host='10.10.10.66', port=10000, server=GeventWebSocketServer)

后端的东西比较的简单,就是把接收到的数据,原路打回去。。。

我前端的代码

这个是连接webscoket,然后接收和发数据的js

<script>
        $(document).ready(function() {
            if (!window.WebSocket) {
                if (window.MozWebSocket) {
                    window.WebSocket = window.MozWebSocket;
                } else {
                    $('#messages').append("<li>Your browser doesn't support WebSockets.</li>");
                }
            }
            ws = new WebSocket('ws://10.10.10.66:10000/websocket');
            ws.onopen = function(evt) {
                $('#messages').append('<li>Connected to chat.</li>');
            }
            ws.onmessage = function(evt) {
                $('#messages').append('<li>' + evt.data + '</li>');
            }
            $('#send-message').submit(function() {
                ws.send($('#name').val() + ": " + $('#message').val());
                $('#message').val('').focus();
                return false;
            });
        });
    </script>

用来呈现结果的div

form id="send-message" class="form-inline">
        <input id="name" type="text" value="可以更换名字">
        <input id="message" type="text" value="要扯淡的内容" />
         <button class="btn btn-success" type="submit">Send</button>
    </form>
    <div id="messages"></div>

这里有个tornado后端的代码,实现的过程和我差不多的~我需要的朋友可以跑一下~

import logging
import os.path
import uuid
import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web
import tornado.websocket
def send_message(message):
    for handler in ChatSocketHandler.socket_handlers:
        try:
            handler.write_message(message)
        except:
            logging.error('Error sending message', exc_info=True)
class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.render('index.html')
class ChatSocketHandler(tornado.websocket.WebSocketHandler):
    socket_handlers = set()
    def open(self):
        ChatSocketHandler.socket_handlers.add(self)
        send_message('A new user has entered the chat room.')
    def on_close(self):
        ChatSocketHandler.socket_handlers.remove(self)
        send_message('A user has left the chat room.')
    def on_message(self, message):
        send_message(message)
def main():
    settings = {
        'template_path': os.path.join(os.path.dirname(__file__), 'templates'),
        'static_path': os.path.join(os.path.dirname(__file__), 'static')
    }
    application = tornado.web.Application([
        ('/', MainHandler),
        ('/new-msg/', ChatHandler),
        ('/new-msg/socket', ChatSocketHandler)
    ], **settings)
    http_server = tornado.httpserver.HTTPServer(application)
    http_server.listen(8000)
    tornado.ioloop.IOLoop.instance().start()
if __name__ == '__main__':
    main()

我和沈灿的对话~

未分类

沈灿和我的对话

未分类

Python爬取糗事百科实践

大家好,前面入门已经说了那么多基础知识了,下面我们做几个实战项目来挑战一下吧。那么这次为大家带来,Python爬取糗事百科的小段子的例子。

首先,糗事百科大家都听说过吧?糗友们发的搞笑的段子一抓一大把,这次我们尝试一下用爬虫把他们抓取下来。

友情提示

糗事百科在前一段时间进行了改版,导致之前的代码没法用了,会导致无法输出和CPU占用过高的情况,是因为正则表达式没有匹配到的缘故。
现在,博主已经对程序进行了重新修改,代码亲测可用,包括截图和说明,之前一直在忙所以没有及时更新,望大家海涵!
更新时间:2015/8/2

糗事百科又又又又改版了,博主已经没心再去一次次匹配它了,如果大家遇到长时间运行不出结果也不报错的情况,请大家参考最新的评论,热心小伙伴提供的正则来修改下吧~
更新时间:2016/3/27

本篇目标

  • 抓取糗事百科热门段子
  • 过滤带有图片的段子
  • 实现每按一次回车显示一个段子的发布时间,发布人,段子内容,点赞数。

糗事百科是不需要登录的,所以也没必要用到Cookie,另外糗事百科有的段子是附图的,我们把图抓下来图片不便于显示,那么我们就尝试过滤掉有图的段子吧。

好,现在我们尝试抓取一下糗事百科的热门段子吧,每按下一次回车我们显示一个段子。

1. 确定URL并抓取页面代码

首先我们确定好页面的URL是 http://www.qiushibaike.com/hot/page/1,其中最后一个数字1代表页数,我们可以传入不同的值来获得某一页的段子内容。

我们初步构建如下的代码来打印页面代码内容试试看,先构造最基本的页面抓取方式,看看会不会成功

# -*- coding:utf-8 -*-
import urllib
import urllib2


page = 1
url = 'http://www.qiushibaike.com/hot/page/' + str(page)
try:
    request = urllib2.Request(url)
    response = urllib2.urlopen(request)
    print response.read()
except urllib2.URLError, e:
    if hasattr(e,"code"):
        print e.code
    if hasattr(e,"reason"):
        print e.reason

运行程序,哦不,它竟然报错了,真是时运不济,命途多舛啊

line 373, in _read_status
 raise BadStatusLine(line)
httplib.BadStatusLine: ''

好吧,应该是headers验证的问题,我们加上一个headers验证试试看吧,将代码修改如下

# -*- coding:utf-8 -*-
import urllib
import urllib2

page = 1
url = 'http://www.qiushibaike.com/hot/page/' + str(page)
user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
headers = { 'User-Agent' : user_agent }
try:
    request = urllib2.Request(url,headers = headers)
    response = urllib2.urlopen(request)
    print response.read()
except urllib2.URLError, e:
    if hasattr(e,"code"):
        print e.code
    if hasattr(e,"reason"):
        print e.reason

嘿嘿,这次运行终于正常了,打印出了第一页的HTML代码,大家可以运行下代码试试看。在这里运行结果太长就不贴了。

2. 提取某一页的所有段子

好,获取了HTML代码之后,我们开始分析怎样获取某一页的所有段子。

首先我们审查元素看一下,按浏览器的F12,截图如下

未分类

我们可以看到,每一个段子都是

包裹的内容。

现在我们想获取发布人,发布日期,段子内容,以及点赞的个数。不过另外注意的是,段子有些是带图片的,如果我们想在控制台显示图片是不现实的,所以我们直接把带有图片的段子给它剔除掉,只保存仅含文本的段子。

所以我们加入如下正则表达式来匹配一下,用到的方法是 re.findall 是找寻所有匹配的内容。方法的用法详情可以看前面说的正则表达式的介绍。

好,我们的正则表达式匹配语句书写如下,在原来的基础上追加如下代码

content = response.read().decode('utf-8')
pattern = re.compile('<div.*?author">.*?<a.*?<img.*?>(.*?)</a>.*?<div.*?'+
                         'content">(.*?)<!--(.*?)-->.*?</div>(.*?)<div class="stats.*?class="number">(.*?)</i>',re.S)
items = re.findall(pattern,content)
for item in items:
    print item[0],item[1],item[2],item[3],item[4]

现在正则表达式在这里稍作说明

  • .*? 是一个固定的搭配,.和代表可以匹配任意无限多个字符,加上?表示使用非贪婪模式进行匹配,也就是我们会尽可能短地做匹配,以后我们还会大量用到 .? 的搭配。

  • (.*?)代表一个分组,在这个正则表达式中我们匹配了五个分组,在后面的遍历item中,item[0]就代表第一个(.?)所指代的内容,item[1]就代表第二个(.?)所指代的内容,以此类推。

  • re.S 标志代表在匹配时为点任意匹配模式,点 . 也可以代表换行符。

这样我们就获取了发布人,发布时间,发布内容,附加图片以及点赞数。

在这里注意一下,我们要获取的内容如果是带有图片,直接输出出来比较繁琐,所以这里我们只获取不带图片的段子就好了。

所以,在这里我们就需要对带图片的段子进行过滤。

我们可以发现,带有图片的段子会带有类似下面的代码,而不带图片的则没有,所以,我们的正则表达式的item[3]就是获取了下面的内容,如果不带图片,item[3]获取的内容便是空。

<div class="thumb">

<a href="/article/112061287?list=hot&s=4794990" target="_blank">
<img src="http://pic.qiushibaike.com/system/pictures/11206/112061287/medium/app112061287.jpg" alt="但他们依然乐观">
</a>

</div>

所以我们只需要判断item[3]中是否含有img标签就可以了。

好,我们再把上述代码中的for循环改为下面的样子

for item in items:
        haveImg = re.search("img",item[3])
        if not haveImg:
            print item[0],item[1],item[2],item[4]

现在,整体的代码如下

# -*- coding:utf-8 -*-
import urllib
import urllib2
import re

page = 1
url = 'http://www.qiushibaike.com/hot/page/' + str(page)
user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
headers = { 'User-Agent' : user_agent }
try:
    request = urllib2.Request(url,headers = headers)
    response = urllib2.urlopen(request)
    content = response.read().decode('utf-8')
    pattern = re.compile('<div.*?author">.*?<a.*?<img.*?>(.*?)</a>.*?<div.*?'+
                         'content">(.*?)<!--(.*?)-->.*?</div>(.*?)<div class="stats.*?class="number">(.*?)</i>',re.S)
    items = re.findall(pattern,content)
    for item in items:
        haveImg = re.search("img",item[3])
        if not haveImg:
            print item[0],item[1],item[2],item[4]
except urllib2.URLError, e:
    if hasattr(e,"code"):
        print e.code
    if hasattr(e,"reason"):
        print e.reason

运行一下看下效果

未分类

恩,带有图片的段子已经被剔除啦。是不是很开森?

3. 完善交互,设计面向对象模式

好啦,现在最核心的部分我们已经完成啦,剩下的就是修一下边边角角的东西,我们想达到的目的是:

按下回车,读取一个段子,显示出段子的发布人,发布日期,内容以及点赞个数。

另外我们需要设计面向对象模式,引入类和方法,将代码做一下优化和封装,最后,我们的代码如下所示

__author__ = 'CQC'
# -*- coding:utf-8 -*-
import urllib
import urllib2
import re
import thread
import time

#糗事百科爬虫类
class QSBK:

    #初始化方法,定义一些变量
    def __init__(self):
        self.pageIndex = 1
        self.user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
        #初始化headers
        self.headers = { 'User-Agent' : self.user_agent }
        #存放段子的变量,每一个元素是每一页的段子们
        self.stories = []
        #存放程序是否继续运行的变量
        self.enable = False
    #传入某一页的索引获得页面代码
    def getPage(self,pageIndex):
        try:
            url = 'http://www.qiushibaike.com/hot/page/' + str(pageIndex)
            #构建请求的request
            request = urllib2.Request(url,headers = self.headers)
            #利用urlopen获取页面代码
            response = urllib2.urlopen(request)
            #将页面转化为UTF-8编码
            pageCode = response.read().decode('utf-8')
            return pageCode

        except urllib2.URLError, e:
            if hasattr(e,"reason"):
                print u"连接糗事百科失败,错误原因",e.reason
                return None


    #传入某一页代码,返回本页不带图片的段子列表
    def getPageItems(self,pageIndex):
        pageCode = self.getPage(pageIndex)
        if not pageCode:
            print "页面加载失败...."
            return None
        pattern = re.compile('<div.*?author">.*?<a.*?<img.*?>(.*?)</a>.*?<div.*?'+
                         'content">(.*?)<!--(.*?)-->.*?</div>(.*?)<div class="stats.*?class="number">(.*?)</i>',re.S)
        items = re.findall(pattern,pageCode)
        #用来存储每页的段子们
        pageStories = []
        #遍历正则表达式匹配的信息
        for item in items:
            #是否含有图片
            haveImg = re.search("img",item[3])
            #如果不含有图片,把它加入list中
            if not haveImg:
                replaceBR = re.compile('<br/>')
                text = re.sub(replaceBR,"n",item[1])
                #item[0]是一个段子的发布者,item[1]是内容,item[2]是发布时间,item[4]是点赞数
                pageStories.append([item[0].strip(),text.strip(),item[2].strip(),item[4].strip()])
        return pageStories

    #加载并提取页面的内容,加入到列表中
    def loadPage(self):
        #如果当前未看的页数少于2页,则加载新一页
        if self.enable == True:
            if len(self.stories) < 2:
                #获取新一页
                pageStories = self.getPageItems(self.pageIndex)
                #将该页的段子存放到全局list中
                if pageStories:
                    self.stories.append(pageStories)
                    #获取完之后页码索引加一,表示下次读取下一页
                    self.pageIndex += 1

    #调用该方法,每次敲回车打印输出一个段子
    def getOneStory(self,pageStories,page):
        #遍历一页的段子
        for story in pageStories:
            #等待用户输入
            input = raw_input()
            #每当输入回车一次,判断一下是否要加载新页面
            self.loadPage()
            #如果输入Q则程序结束
            if input == "Q":
                self.enable = False
                return
            print u"第%d页t发布人:%st发布时间:%st赞:%sn%s" %(page,story[0],story[2],story[3],story[1])

    #开始方法
    def start(self):
        print u"正在读取糗事百科,按回车查看新段子,Q退出"
        #使变量为True,程序可以正常运行
        self.enable = True
        #先加载一页内容
        self.loadPage()
        #局部变量,控制当前读到了第几页
        nowPage = 0
        while self.enable:
            if len(self.stories)>0:
                #从全局list中获取一页的段子
                pageStories = self.stories[0]
                #当前读到的页数加一
                nowPage += 1
                #将全局list中第一个元素删除,因为已经取出
                del self.stories[0]
                #输出该页的段子
                self.getOneStory(pageStories,nowPage)


spider = QSBK()
spider.start()

好啦,大家来测试一下吧,点一下回车会输出一个段子,包括发布人,发布时间,段子内容以及点赞数,是不是感觉爽爆了!

我们第一个爬虫实战项目介绍到这里,欢迎大家继续关注,小伙伴们加油!

使用进程池规避Python的GIL限制

  • 操作系统 : CentOS7.3.1611_x64

  • python版本:2.7.5

问题描述

Python的GIL会对CPU密集型的程序产生影响,如果完全使用Python来编程,怎么避开GIL的限制呢?

解决方案

在多线程中使用进程池来规避GIL的限制。具体如下:

1、使用multiprocessing模块来创建进程池;

2、将计算任务分配给不同的线程;

3、在任务线程中把任务提交给之前创建的进程池;

每当有线程要执行cpu密集型任务时,就把该任务提交到进程池中,然后进程池会将任务交给运行在另一个进程中的Python解释器。

当线程等待结果时会释放GIL,而此时的计算任务是在另一个单独的Python解释器中执行的,不再受到GIL的限制了。

在多核系统中采用这个方案能轻易地利用到所有的CPU核心。

假设有这样的worker函数:

def worker(arr):
    s = 0
    for n in arr :
        arrTmp = range(1,n+1)
        if len(arrTmp) == 0 : continue
        rtmp = 1
        for i in arrTmp :
            rtmp *= i
        s += rtmp
    return s

完整代码如下:https://github.com/mike-zhang/pyExamples/blob/master/gilAvoid/gilAvoidTest1/taskCommon.py

普通单进程任务实现:

def main():
    s = 0
    tStart,tStop = 1,1000
    for i in range(1,100):
        #t = worker(range(tStart,tStop))
        t = worker(range(1,1000))
        s += t
        tStart = tStop
        tStop += 1000
    print("len : ",len(str(s)))
    print(s%10000)

完整代码如下: https://github.com/mike-zhang/pyExamples/blob/master/gilAvoid/gilAvoidTest1/t1_normal.py

运行效果如下:

(py27env) [mike@localhost test]$ time python t1_normal.py
('len : ', 2567)
987

real    0m17.919s
user    0m17.915s
sys     0m0.003s

使用进程池的实现:

def wokerThread(start,stop):
    #r = gPool.apply(worker,(range(start,stop),))
    r = gPool.apply(worker,(range(1,1000),))
    q.put(r)

def main():
    s = 0
    thrdArr = []
    tStart,tStop = 1,1000
    for i in range(1,gCount+1):
        thrd = threading.Thread(target=wokerThread,args=(tStart,tStop))
        thrdArr.append(thrd)
        tStart = tStop
        tStop += 1000
    for t in thrdArr :
        t.daemon = True
        t.start()
    while not q.full(): time.sleep(0.1)
    while not q.empty(): s += q.get()
    print("len : ",len(str(s)))
    print(s%10000)

完整代码如下:https://github.com/mike-zhang/pyExamples/blob/master/gilAvoid/gilAvoidTest1/t2_mp.py

运行效果如下:

(py27env) [mike@localhost test]$ time python t2_mp.py
queue full
('len : ', 2567)
987

real    0m4.917s
user    0m18.356s
sys     0m0.146s

可以看出使用上述方法可以规避GIL的限制(测试机器为i5 4核),程序的速度得到明显的提升。

好,就这些了,希望对你有帮助。

本文github地址:

https://github.com/mike-zhang/mikeBlogEssays/blob/master/2017/20170819_使用进程池规避python的GIL限制.rst

Python服务端内存泄露的处理过程

吐槽

内存泄露 ? 内存暴涨 ? OOM ?

首先提一下我自己曾经历过多次内存泄露,到底有几次? 我自己心里悲伤的回想了下,造成线上影响的内存泄露事件有将近5次了,没上线就查出内存暴涨次数可能更多。这次不是最惨,相信也不会是最后的内存的泄露。

有人说,内存泄露对于程序员来说,是个好事,也是个坏事。 怎么说? 好事在于,技术又有所长进,经验有所心得…. 毕竟不是所有程序员都写过OOM的服务…. 坏事当然就是被人吐槽了…. 回想了下被OOM的服务,并发和数据量级都相对大,也就是说,并发量大的服务容易出现OOM…

说的自己都乐了,为毛每次写服务端和客户端的时候,我很大几率会出现内存泄露呢…. 一路走来发现内存暴涨的原因,一般是数据的边界条件处理不合理导致的,而单纯因为某个模块及框架bug代码导致的内存泄露相对少。

废话不多说,简单说下这次的处理过程.

ops的同事给我打电话,说我的服务挂了… 作为肇事者的我说,不应该呀,你再启动下呀? 那哥们说,启动了,一会又死了… 我说,再启动下呀? 一会ops回复说, 又死了… 卧槽呀,这不是个好现象呀….

没办法,哥只能亲自上了…. 首先确认下是什么原因导致的程序总是挂掉, 因为我的服务端是Master Worker设计模式,在顶级的函数入口做了try catch,协程入口也做了try catch,所以程序的逻辑异常肯定不会导致这个进程崩溃的。进程的日志没有特别明显的异常信息…

怀疑是被oom了,dmesg看到的信息果然是被oom了… 那么这里有几个问题:

  • 为什么会被oom

  • 什么时候被oom的,内存持续了多久?

  • oom应该干掉泄露内存的worker进程,为什么把 worker 和 feeds进程都干掉了…

然后我们开始确认下内存的增长趋势,及相关的监控图表。 通过监控服务器的内存图表,我们得知,内存的增长是爆发性的,后期根据我自己的内存监控脚本显示,从500m内存 干到 16G,再到被内核OOM,最快时候一分钟完事。

另外通过流量图表,我们可以找到其影响关系,没次内存暴涨的时候,流量也很高…. 但根据我程序的计算显示qps并不高,但是内存暴增的厉害,流量图徒增的也很厉害… 我这个服务是做全网cdn刷新预缓存的业务,操作cdn只是触发header请求而已,按照道理不会有太大的流量。

这内存暴增的问题不是时常出现的,有可能几天才出现一次。 在内存暴增之后,追问题其实相对麻烦的,只能看日志。 最简单直接的方法是,事故发生时,在线调….. (这需要我时刻关注监控服务情况,第一时间做在线追问题)….

服务上线之前,我们肯定经过疯狂的压力和稳定性测试,但是从来没有出现过内存泄露问题,很是疑惑的…

当遇到问题时,我先通过strace看看有什么不良的系统调用,发现有大量的recvfrom… recvfrom系统调用是用来从socket读取数据的… 为什么会有大量的数据recv呢 ? tcpdump抓包分析出有大量的数据是二进制的…. 但我的服务只是发送http header请求呀…. 通过不断的抓包和iftop流量分析,得出 http get 请求大文件导致的内存暴增问题…

# xiaorui.cc

% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 99.56    1.158373           6    189545           recvfrom
  0.44    0.005162           0     18301           brk
  0.00    0.000000           0        23           read
  0.00    0.000000           0        21           write
  0.00    0.000000           0         6           open
  0.00    0.000000           0         8           close
  0.00    0.000000           0         6           fstat
  0.00    0.000000           0        10         3 mmap

分析数据得知,明显是二进制…. 正常接口不会出现二进制的数据流…

# xiaorui.cc

recvfrom(3, "207343c320fD2014264363rE^34350a;177322222[H`n364302315!=247 "..., 10240, 0, NULL, NULL) = 3358
recvfrom(3, "R,C45c23M30522531&327136424422536204302304D237240367315`331]e234345"..., 6882, 0, NULL, NULL) = 6882
recvfrom(3, "207S3312252243649202326[ 375362"N30133420F332f234"2706321q361207wv"..., 10240, 0, NULL, NULL) = 10240
recvfrom(3, "214H303A*377I,256221316gF4244274234t260265217317<r307<G=255A30246"..., 10240, 0, NULL, NULL) = 10240
recvfrom(3, "zT31235317722*q26326tq341274236323<Cb34510341NA}7252222c203%"..., 10240, 0, NULL, NULL) = 6218
recvfrom(3, "22425241W$F233317350L347244361[271262[^347274333221ng36s266324313Ug275"..., 4022, 0, NULL, NULL) = 4022
recvfrom(3, "U341310210]w250C%2330375364R274372Z726334026331325302224376300260zPy271"..., 10240, 0, NULL, NULL) = 10240

我们知道大多数cdn厂商给我们开放了不同的http method方法,每个方法有不同的意思…. 除了get请求外,大多请求都是异步的…. 但是get是同步的, 既然是同步的,那么你直接用python requests访问一个大文件,必然是需要把数据载入到内存中…. 对的,内存中…. 就算你只是想看看response的http code,但对于一些http client module来说,还是需要加载完所有buffer才会解析成header….

那么怎么解决? requests 针对一些大文件的get请求 改成 写文件的方式请求… 这样就避免了,你请求了 1G 大小的文件,必须拿到所有数据后,才可以return的逻辑… 你把实时recv到内存,改成append到文件里…. 但这样就好了么? 不,还是会有内存的问题… 当然相比直接gevent requests get 好不少….. 另外,我最后加了手动触发gc,这样内存能得到及时释放,我们知道python gc 是有thold刷新策略的。

我最后的解决方式是什么? 我是采用gevent subprocess调用curl来处理大文件…. 为什么用这么粗暴的方式来系统调用curl来处理…

curl是 c 写的,性能极棒 !!! 针对大文件请求,cpu消耗同比python requests少的多… 有些人说了,处理大文件的请求明显是 io bound,怎么可能浪费cpu呢,这也没有啥cpu bound密集活动呀…. 我们多进程加多协程的框架,协程之间的调度需要资源,http parser需要资源…. requests库纯py写的http parser…. 你访问一两个文件肯定没问题了,但是你高并发去处理这类请求,你python requests总是占用cpu时间,那自然就cpu占用率高了,你高了,自然别人就拿不到时间片调度了,只能等了….. 大家可以做一些量级的测试对比….

未分类

为什么某个子进程被oom了,父进程也跟着退出,而且其他进程是友好退出…. 所谓的友好的退出意思是,每个进程也都有任务队列缓冲,当得知要退出时,他会把队列中的数据退还给redis,并且等待正在执行的任务完成后才退出….

问题在于我在父进程做了很多信号的处理,子进程当然继承了父进程的信号处理,当获取不到内存时,会触发SIGINT信号,对的…. 首先是SIGINT,然后才有SIGKILL ( -9 ). 当我收到sigint时,会给一个共享变量配置一个标记,所以其他进程会出现安全退出的情况….

rt_sigaction(SIGINT, {SIG_DFL, [], SA_RESTORER, 0x33efc0f7e0}, {0x4e7de0, [], SA_RESTORER, 0x33efc0f7e0}, 8) = 0
+++ exited with 1 +++

但可能当我自己还没认识到内存不够用,有人向内核申请内存,内核说没有,但是我可以干掉分值最低,占内存最大的进程…. 这就造成了OOM….. sigkill是无法捕获的….

针对子进程由于copy on write继承父进程的信号配置,我们可以在子进程fork之后,再重新初始化信号配置… 这样子进程不管什么原因退出,都不管影响到master主进程, 主进程会定时做子进程轮询,如果某子进程挂了,他会重置一个新内存放在进程池里面…. 这方面借鉴了uwsgi的设计…

END…

Python使用os、try或pathlib判断文件是否存在

通常在读写文件之前,需要判断文件或目录是否存在,不然某些处理方法可能会使程序出错。所以最好在做任何操作之前,先判断文件是否存在。

这里将介绍三种判断文件或文件夹是否存在的方法,分别使用os模块、Try语句、pathlib模块。

1. 使用os模块

os模块中的os.path.exists()方法用于检验文件是否存在。

  • 判断文件是否存在
import os
os.path.exists(test_file.txt)
#True

os.path.exists(no_exist_file.txt)
#False
  • 判断文件夹是否存在
import os
os.path.exists(test_dir)
#True

os.path.exists(no_exist_dir)
#False

可以看出用os.path.exists()方法,判断文件和文件夹是一样。

其实这种方法还是有个问题,假设你想检查文件“test_data”是否存在,但是当前路径下有个叫“test_data”的文件夹,这样就可能出现误判。为了避免这样的情况,可以这样:

  • 只检查文件
import os
os.path.isfile("test-data")

通过这个方法,如果文件”test-data”不存在将返回False,反之返回True。

即是文件存在,你可能还需要判断文件是否可进行读写操作。

判断文件是否可做读写操作

使用os.access()方法判断文件是否可进行读写操作。

语法:

os.access(path, mode)

path为文件路径,mode为操作模式,有这么几种:

  • os.F_OK: 检查文件是否存在;
  • os.R_OK: 检查文件是否可读;
  • os.W_OK: 检查文件是否可以写入;
  • os.X_OK: 检查文件是否可以执行

该方法通过判断文件路径是否存在和各种访问模式的权限返回True或者False。

import os
if os.access("/file/path/foo.txt", os.F_OK):
    print "Given file path is exist."

if os.access("/file/path/foo.txt", os.R_OK):
    print "File is accessible to read"

if os.access("/file/path/foo.txt", os.W_OK):
    print "File is accessible to write"

if os.access("/file/path/foo.txt", os.X_OK):
    print "File is accessible to execute"

2. 使用Try语句

可以在程序中直接使用open()方法来检查文件是否存在和可读写。

语法:

open(<file/path>)

如果你open的文件不存在,程序会抛出错误,使用try语句来捕获这个错误。

程序无法访问文件,可能有很多原因:

  • 如果你open的文件不存在,将抛出一个FileNotFoundError的异常;
  • 文件存在,但是没有权限访问,会抛出一个PersmissionError的异常。

所以可以使用下面的代码来判断文件是否存在:

try:
    f =open()
    f.close()
except FileNotFoundError:
    print "File is not found."
except PersmissionError:
    print "You don't have permission to access this file."

其实没有必要去这么细致的处理每个异常,上面的这两个异常都是IOError的子类。所以可以将程序简化一下:

try:
    f =open()
    f.close()
except IOError:
    print "File is not accessible."

使用try语句进行判断,处理所有异常非常简单和优雅的。而且相比其他不需要引入其他外部模块。

3. 使用pathlib模块

pathlib模块在Python3版本中是内建模块,但是在Python2中是需要单独安装三方模块。

使用pathlib需要先使用文件路径来创建path对象。此路径可以是文件名或目录路径。

  • 检查路径是否存在
path = pathlib.Path("path/file")
path.exist()
  • 检查路径是否是文件
path = pathlib.Path("path/file")
path.is_file()

你应该使用容器吗(容器化vs虚拟化)?

【编者的话】究竟什么情况下应该使用容器,什么情况下使用虚拟机?让我们把容器的优势一一列出,再看它是否能适应你的生态系统。

近年来,随着高端技术崭露头角并逐渐取代传统系统,虚拟化需求也迅速扩大。同时,云计算的崛起在这项横扫业界的技术发展过程中起到了催化剂的作用。

容器化已经不是一个新概念,通过将应用程序封装在容器自有的操作环境中,使其独立于平台,同时更加方便使用。目前,所有顶尖的云计算厂商,如AWS,Google,Microsoft等等,都在提供云容器服务。

容器化的潜在优势

在Docker推出开源容器技术,并有AWS向其提供容器服务支持之后,容器化技术的发展势头更劲。这项技术能带来的优势如下:

  • 平台独立性: 如将一个应用的所有依赖都封装到容器中,该应用便可以在各种机器上自由运行。这体现了该技术的高度可移植性。

  • 不再需要重写代码: 站在公司的角度,容器化带来的最大优势是开发者们不再需要一遍又一遍地为每个不同的平台重写代码。这样一来,不仅省时省钱,还省力。

  • 最适合内部应用的开发: 应用的容器化最适合公司内部应用的开发。它提供了一个和生产环境一致的开发环境,防止由于环境差异导致的功能失效,而且有助于移植应用。

  • 划分任意应用: 不仅有助于划分内部应用,针对任何种类的应用,容器化都能成功将其划分到微服务中,提高整个系统的速度。

  • 轻量: 在移植应用的过程中,不需要将操作环境也封装进容器中同步移植,因此,容器是相当轻量的。容器中只有应用,使其更加简洁轻量。

容器化vs虚拟化

上面对容器化的描述听起来跟虚拟化是不是挺像的?确实,这个概念就是从虚拟化发展过来的,但两者之间还是存在着一些区别的。下面我们来对两者做个比较:

未分类

既然虚拟化是机器的抽象,你也可以将容器看做一种升级版虚拟机。

容器的挑战

Docker的发展以及容器的实现带来的种种优势,让这项技术名声大振。但是,容器的部署过程中也存在着一些挑战。

  • 一个操作系统的崩溃会破会整个系统: 由于所有的容器都访问同一个操作系统,所以容器是资源高效的。但正是这种机制下,操作系统的崩溃会导致所有的容器崩溃。

  • 兼容性问题: 迁移容器的过程十分反锁,因为要考虑到服务器操作系统是否兼容的问题,这就限制其迁移能力。

  • 并不适用于所有任务: 容器化适用于由小程序块构成,可以被拆分成微服务的应用。但是,有些应用却不是这样,因此也不好拆分。不过,这些应用还是可以在虚拟机上运行。

如何确定你是否应该使用容器

这完全取决于你的需求。如果你需要运行同一个应用的多个实例,那么容器会有很大帮助。但如果你想要运行多个应用,那你应该选择虚拟机。

如果你只能运行一个操作系统,容器还是很有用的。同时,如果你对运行各种操作系统没有限制,那虚拟机对你来说是更好的选择。来回考量这些区别,可能你想同时选择这两种技术,确实他们可以在同一个系统中共存,并服务于公司的不同任务。

虽然有种种漏洞,这项新兴技术已经成功占据了市场,将近80的组织正在使用容器,其中68%的组织相信,他们正在实现期望中的目标。——ClusterHQ

ClusterHQ还表示,48%的企业正在使用AWS。其次是 Internal Data Centre占有28%的市场,第三位是有着7%市场占有率的Google Compute Engine。

但是,这项技术仍然处于起步阶段,它将为更多的尖端技术创造空间,一起来颠覆数字经济。

nginx 1.13.3编译安装ngx_lua_waf安全防护

未分类

前言

ngx_lua_waf是一个基于ngx_lua的web应用防火墙,使用简单,高性能和轻量级。

  • 防止sql注入,本地包含,部分溢出,fuzzing测试,xss,SSRF等web攻击

  • 防止svn/备份之类文件泄漏

  • 防止ApacheBench之类压力测试工具的攻击

  • 屏蔽常见的扫描黑客工具,扫描器

  • 屏蔽异常的网络请求

  • 屏蔽图片附件类目录php执行权限

  • 防止webshell上传

准备

ngx_lua_waf推荐使用Luajit做lua支持,而Luajit需要ngx_devel_kit模块与lua-nginx-module模块。进入/usr/local/src目录准备以上文件。

1、下载ngx_devel_kit

wget https://github.com/simpl/ngx_devel_kit/archive/v0.3.0.tar.gz
tar zxvf v0.3.0.tar.gz

2、下载lua-nginx-module

wget https://github.com/openresty/lua-nginx-module/archive/v0.10.10.tar.gz
tar zxvf v0.10.10.tar.gz

3、安装Lua环境

wget http://luajit.org/download/LuaJIT-2.0.3.tar.gz
tar xf LuaJIT-2.0.5.tar.gz
cd LuaJIT-2.0.5
make && sudo make install

编译

1、进入Nginx安装目录:

cd /usr/local/src/nginx-1.13.3

2、导入环境变量

#这个很有可能不一样
export LUAJIT_LIB=/usr/local/lib
#这个很有可能不一样
export LUAJIT_INC=/usr/local/include/luajit-2.0

3、查看目前Nginx的版本

nginx -V

4、加载模块

./configure(Nginx-V已有的模块)--add-module=/usr/local/src/ngx_devel_kit-0.3.0 --add-module=/usr/local/src/lua-nginx-module-0.10.10 --with-ld-opt=-Wl,-rpath,$LUAJIT_LIB

5、编译参数

# 注意不要make install
make
#重命名nginx旧版本二进制文件
mv /usr/local/nginx/sbin/nginx /usr/local/nginx/sbin/nginx.old
#拷贝一份新编译的二进制文件
cp objs/nginx /usr/local/nginx/sbin/
#升级
make upgrade
#确认
nginx -V
#重新加载Nginx
nginx -s reload

安装

1、准备Luajit

从https://github.com/loveshell/ngx_lua_waf下载Luajit。

nginx安装路径假设为/usr/local/nginx/,把ngx_lua_waf下载到conf目录下,解压命名为waf。

2、在nginx.conf的http段添加

lua_package_path "/usr/local/nginx/conf/waf/?.lua";
lua_shared_dict limit 10m;
init_by_lua_file  /usr/local/nginx/conf/waf/init.lua;
access_by_lua_file /usr/local/nginx/conf/waf/waf.lua;

配置config.lua里的waf规则目录(一般在waf/conf/目录下),绝对路径如有变动,需对应修改:

RulePath = "/usr/local/nginx/conf/waf/wafconf/"

然后重启nginx即可

service nginx restart

配置

1、配置文件详细说明

    RulePath = "/usr/local/nginx/conf/waf/wafconf/"
    --规则存放目录
    attacklog = "on"n
    --是否开启攻击信息记录,需要配置logdir
    logdir = "/home/wwwlogs/"
    --log存储目录,需要nginx用户的可写权限。该目录LNMP默认建立,放在这个目录即可
    --日志文件名称格式如下:虚拟主机名_sec.log
    UrlDeny="on"
    --是否拦截url访问
    Redirect="on"
    --是否拦截后重定向
    CookieMatch = "on"
    --是否拦截cookie攻击
    postMatch = "on"
    --是否拦截post攻击
    whiteModule = "on"
    --是否开启URL白名单
    black_fileExt={"php","jsp"}
    --填写不允许上传文件后缀类型
    ipWhitelist={"127.0.0.1"}
    --ip白名单,多个ip用逗号分隔
    ipBlocklist={"1.0.0.1"}
    --ip黑名单,多个ip用逗号分隔
    CCDeny="on"
    --是否开启拦截cc攻击(需要nginx.conf的http段增加lua_shared_dict limit 10m;)
    CCrate = "100/60"
    --设置cc攻击频率,单位为秒.
    --默认1分钟同一个IP只能请求同一个地址100次
    html=[[Please go away~~]]
    --警告内容,可在中括号内自定义
    备注:不要乱动双引号,区分大小写

2、部署完毕可以尝试如下命令进行测试:

curl https://www.youfencun.com/index.php?id=../etc/passwd

返回网站防火墙Html源码即表示配置成功,也可通过网页访问,返回如下图所示:

未分类

在Ghost博客的nginx服务器上部署防盗链

折腾了一晚上,终于搞定了。搜索了很久都没有搜到有相关的中文文章,甚至英文的都很少,全是generic的nginx部署防盗链的文章,特此总结整理一下方便以后查询。

想必nginx防盗链的部署方法大家都知道了,在装了Ghost的情况下稍稍有所不同,主要因为实际访问的文件不在根目录下,而是在ghost的安装目录中,所以要用到proxy。就因为这一点折腾了好久,症状就是加入防盗链代码后所有图片自动404,加了proxy之后就好了。

以下是我使用的代码:

location /content/ {
  valid_referers none blocked ~.google. ~.bing. ~.baidu. server_names ~($host);
  if ($invalid_referer) {
    rewrite ^/ http://ww1.sinaimg.cn/large/76b129b3gy1fisvu3x79lj21hc0u0ta2.jpg;
     }
   try_files $uri @ghost;
}


location @ghost {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_pass http://127.0.0.1:2368;
}

代码解释如下:

location /content/

↑因为ghost的特殊性,所有上传的图片都在content目录下,如有需求还是改成扫描文件后缀。

valid_referers none blocked ~.google. ~.bing. ~.baidu. server_names ~($host);

↑关于valid_referers网上的解释很多了,推荐阅读 官网的定义(https://nginx.org/en/docs/http/ngx_http_referer_module.html) 。包含了几个搜索引擎,当然不希望图片被搜索引擎爬的也可以去掉,后面使用了host变量方便大家直接复制黏贴。

if ($invalid_referer) 

↑当来源没有列在上述列表中,$invalid_refererfan返回值为1,执行rewrite。

rewrite ^/ http://ww1.sinaimg.cn/large/76b129b3gy1fisvu3x79lj21hc0u0ta2.jpg;

↑rewrite到一个外部图床的防盗链图片,注意用自己的图片网址替换我的网址,如果不需要,可直接 return 444。

try_files $uri @ghost;

↑最重要的一步,因为实际访问的文件并不在服务器的root directory下,本身ghost-cli自动部署的nginx配置就带proxy。try_files的定义请看官方文档。

location @ghost {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_pass http://127.0.0.1:2368;
}

↑这里我照抄了ghost-cli部署时写入的proxy代码。

写入conf之后重启nginx即可。

注:由于本站强制https访问,所以在http段直接设置了所有访问content都rewrite到盗链图片上:

location /content/ {
    rewrite ^/ http://ww1.sinaimg.cn/large/76b129b3gy1fisvu3x79lj21hc0u0ta2.jpg;
}

Nginx反向代理缓存引发的跨域问题

一、前言

贵金属wap版直播间上线后,偶尔有用户反馈,在进入wap直播间的时候,出现空白页面,但是重新刷新又可以正常显示了。我们曾一度认为是网络请求异常或兼容问题,直到开发PC版直播间,在进行调试中,同样遇到了“白屏”问题,才引起了足够重视,并进行了问题跟踪与分析。现在跟大家分享一下,这种偶然现象出现的原因。

我们的直播间落地页在fa.163.com 系统,而直播间内容,是通过 向直播间系统 qz.fa.163.com 发起Ajax请求获取的。在出现“白屏”的时候,可以通过浏览器的调试窗口,可以看到出现下面的报错:

未分类

二、问题分析

从上述错误提示文案中可以看到,问题首先和 跨域 有关。

何为跨域

从字面上理解为“跨域名”,
浏览器不能执行其他网站的脚本,然而,跨域不仅仅局限于域名这一项。只要协议、域名、端口有任何一个不同,都被当作是不同的域。 这是由于>同源策略的限制,从一个域上加载的脚本不允许访问另外一个域的文档属性。虽然在浏览器中,<script>、<img>、<iframe>、<link>等标签都>可>以加载跨域资源,而不受同源限制,但浏览器会限制脚本中发起的跨域请求。比如,使用 XMLHttpRequest 对象和Fetch发起 HTTP 请求就必须遵守同源策略。

同源策略/SOP(Same origin policy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSFR等攻击。SOP要求两个通讯地址的协议、域名、端口号必须相同,否则两个地址的通讯将被浏览器视为不安全的,并被block下来。

举个例子:从贵金属主站 http://fa.163.com 发起请求访问以下url:

未分类

解决跨域

在实际应用中有多种方式来解决跨域问题,相信在实践中都会用到其中的某些方案:

1、JSONP (无状态连接,不能获悉连接状态和错误事件,而且只能走GET的形式)

2、iframe形式

3、服务器代理

页面直接向同域的服务端发请求,服务端进行跨域处理或爬虫后,再把数据返回给客户端页。

4、CORS

CORS(Cross-Origin Resource Sharing)跨域资源共享,定义了必须在访问跨域资源时,浏览器与服务器应该如何沟通。CORS背后的基本思想就>是使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功还是失败。目前,所有浏览器都支持该功能,IE浏览器不能低>于IE10。整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏>览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。

CORS方式实现:

浏览器在发出CORS请求时会在头信息之中增加一个Origin字段;

后端返回代码中增加三个字段

header(“Access-Control-Allow-Origin”:“”);           // 必选 允许所有来源访问
header(“Access-Control-Allow-Credentials”:“true”);  //可选 是否允许发送cookie
header(“Access-Control-Allow-Method”:“POST,GET”);   //可选 允许访问的方式

5、nginx反向代理

nginx是一个高性能的web服务器,常用作反向代理服务器。nginx作为反向代理服务器,就是把http请求转发到另一个或者一些服务器上。通过把本地一个url前缀映射到要跨域访问的web服务器上,就可以。

为了解决跨域问题,我们选择方案d , 在直播间的过滤器中,统一添加了如下代码:

   @Override
   public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    // 加入响应头
    String origin = request.getHeader("Origin");
    if("http://fa.163.com".equals(origin) || "https://fa.163.com".equals(origin) ) {
        response.addHeader("Access-Control-Allow-Origin", origin);
        response.addHeader("Access-Control-Allow-Credentials", "true");
    }
    return true;
}

从错误提示文案中,我们还可以看到错误提示的关键点 “http://fa.163.com” that is not equal to the supplied origin. Origin ‘https://fa.163.com‘ is therefore not allowed access.
目前我们的系统同时支持http访问和https访问,但是为什么使用 http访问 ,返回的header中却是 https 协议呢?

通过多次模拟,确认出现问题的请求中,Request URL使用的协议和 response返回的headers中的 Access-Control-Allow-Origin 中的 协议确实不一致,且还有一个特性,X-Cached 为 HIT,如下图:

未分类

命中了缓存的请求,出现了协议不一致?

突然想到,这个接口,我们配置了nginx 缓存,那必然和nginx缓存有关了。

Nginx 缓存

Nginx (engine x) 是一个高性能的HTTP和反向代理服务器。
首先从源服务器(内部网络上的web服务器)上获取内容,然后把内容返回给用户,同时,也会把内容保存到代理服务器上一份,这样日后再接收同样的信息请求时,他会把本地缓存里的内容直接发给用户,以此减少后端web服务器的压力,提高响应速度。这其实就是缓存服务器所实现的功能。如下图所示。

未分类

进入直播间后,首先需要查询直播内容是否有更新,而这个接口客户端会以5s间隔轮询,为了减少tomcat的压力,我们配置了nginx缓存。配置如下:

未分类

其中:

  • proxy_cache_methods: 用来设置HTTP哪些方法会被缓存,直播间接口配置了GET、HEAD、POST;
  • proxy_cache_valid: 用来设置对不同HTTP状态码的不同缓存时间。直播间接口配置了对于 返回值为200的状态码,缓存5秒;
  • proxy_cache_min_uses: 用来设置多少次访问后,应答值会被缓存,配置为3次;
  • proxy_cache_key: 设置Web缓存的key
  • proxy_cache: 用来设置哪个缓存区将被使用,并定义缓存区的名称

通过上述配置,我们可以看到 proxy_cache_key 配置中,只配置了host + uri + 参数,但没有配置协议,所以无论用http访问,还是https访问,只要被缓存后,返回的内容都是一样的,而不会区分http或https。从而引起了跨域问题。

至此,问题分析完毕。

三、问题解决

跟运维同学沟通后,通过修改nginx配置,将协议类型scheme加入到缓存查找的判断参数中,配置如下。

未分类

问题得到了解决。

四 、总结

上述“惨案” ,是 跨域、nginx缓存、http/https协议 这三种条件同时出现引发的。

如果不涉及跨域,混用 http/https协议 + nginx缓存,其实也是没有问题的。但是一旦出现了跨域使用,必须 在nginx 缓存配置中,配置 scheme + host + uri + 参数。