背景

留言板小程序的后台是采用djando进行开发的,数据库引擎是使用的mysql,由于原阿里云的服务器配置比较低,cpu只有1核,内存只有1G,在用户比较集中访问留言板小程序的时候,容易出现读磁盘io百分百的情况,需要对服务器进行迁移,迁移到腾讯云的服务器上,配置比较高一些,cpu为4核,内存为4G,系统磁盘大小为80G,但原来是在ubuntu上搭建的环境,环境无法直接进行迁移,需要在腾讯云服务器上重新搭建django的环境,考虑到下次迁移的方便性,于是采用docker进行环境搭建和部署。

docker部署

django是采用的python语言,所以直接基于python的docker包进行开发即可,在python的docker镜像包里面安装相关环境,所以直接基于python3.8的镜像制作一个django的dockerfile,需要安装的python软件包放在requirements.txt里面,python镜像是基于ubuntu做的,要安装其他linux环境的软件可以使用apt-get install,注意需要先apt-get update,可以改一下/etc/apt/sources.list里面的下载源地址为阿里云的镜像地址http://mirrors.aliyun.com,这样下载更快,另外python软件包pip install下载也可以加-i参数指定镜像加速,如百度镜像地址https://mirror.baidu.com/pypi/simple,这样django的dockerfile就完成了,只有mysql直接使用官方的docker镜像即可,nginx也是,但这些docker要怎么编排呢,由于是在同一台宿主机协调工作,采用docker-compose即可完美解决。

mysql设置

这里注意要配置mysql的字符集,docker-compose.yml里面mysql部分的command要配置–character-set-server=utf8mb4 –collaction-server=utf8mb4_unicode_ci,否则mysql的表里面写不了中文字符。

编排时,mysql预先启动,然后是django,最后是nginx,django要等待mysql启动正常后才能启动,可以用depends_on参数,但是还不够,需要检测到mysql可以正常连接才行,有个wait-for.sh的脚本可以参考。

docker里面需要的一些环境变量可以通过environment传递进去,而docker-compose.yml里面的变量需要在.env隐藏文件里面添加进去,注意docker里面的crontab是拿不到这些环境变量的,需要env > /etc/environment才可以。

django要去连接mysql通过host就行,host就是容器的名字,这样就可以直接走网络了,后面又加了redis,连接的host也是容器的名字,不然的话,就只能走unix socket通信,如果不同容器之间用户不一致的话,这个.sock文件就需要777的权限了,否则另外的程序访问不了,比如这里uwsgi.sock需要被nginx访问,而nginx是以nginx用户运行的,这种情况需要在uwsgi.ini文件里面设置chmod-socket为777。

nginx设置

nginx的template配置可以放在/etc/nginx/templates目录下,由于conf.d目录以及sites-available目录下均可以配置server的配置,因为都会被/etc/nginx/nginx.conf文件include,nginx镜像在启动的时候会调用docker-entrypoint.d里面的20-envsubst-on-templates.sh脚本将/etc/nginx/templates文件夹里面的template配置解析出来放到conf.d目录,这个过程可以解析环境变量,所以可以在template的配置里面配置环境变量。

nginx的配置可以配置多个server,每个server监听不同的server_name或者端口,但一个端口只能配置一个default_server,http的请求可以rewite到https上,用rewrite ^(.*)$ https://$host$1 permanent;

缓存优化

到这里,基本的docker-compose相关的配置都差不多了,后面通过docker-compose up -d启动即可,下面介绍下缓存的优化,因为在流量大的时候,留言板大部分都是在读取数据,如果每次都从mysql里面去读,这样会造成磁盘读的压力大,iocpu就会很高,所以需要加缓存来做保护,mysql是很脆弱的,目前django提供的缓存策略有几种,一个是磁盘文件做缓存,这种效果不佳,还是要去读磁盘,一种是数据库表做缓存,这种也是要去读磁盘,一种是本地内存做缓存,这种稍微好点,但是当你是多进程执行的时候,每个进程都有自己的缓存,缓存不是共享的,也有问题,只能说是解决了一部分问题,最好的一种是采用第三方平台做缓存,比如memcached或者redis,这样无论你是多线程还是多进程,缓存文件都只有一份,我这里采用redis来进行缓存优化。

django提供了站点缓存和页面缓存的方式,站点缓存需要在settings中配置中间件,页面缓存可以使用路由url的方式和视图view的方式,都是采用cache_page的装饰方法,而如果要做到更加细致化的缓存需要自己在api方法中加入缓存set,或者编写自己的装饰方法,一般一个方法中只有一个cache_key的时候可以编写方法装饰器,这样最方便,否则只能在方法中手动调用cache.set,缓存的基本思想就是,读数据的时候,先从缓存读,读到了就直接返回,没有读到,再从mysql读,读完缓存起来,下次就可以从缓存读了,需要设置缓存的过期时间,如果有用户写,那原来的缓存就需要清理掉,因为缓存需要保持最新的数据。我这里需要在docker-compose.yml里面添加redis的service,另外django需要安装django-redis的软件,相当于redis的客户端。后面在settings中设置好CACHE为redis后,就可以直接调用cache.set,cache.get,cache.delete_pattern进行访问了,还可以设置连接池,这样连接redis时不需要每次都新建连接。

redis分布式锁

有了redis这个中间件,就可以用它的分布式锁了,正好解决我之前遗留的一个问题:小程序用一个用户可能会并发发起两个login请求,这个时候如果用户之前不存在,需要新建用户,而新建用户时的username是通过uuid随机指定的,这样就可能造成创建了两个相同openid的用户,这是有问题的,针对并发问题,需要让后到的那个请求先等待用户创建完,再去读取之前创建的用户才行,但django是用uwsgi启动的,既有多线程,也有多进程,所以两个并发的login请求可能在不同的进程或者线程里面得到处理,普通的锁无法做到同步,这个时候redis的分布式锁就起到效果了,直接用with cache.lock(openid)即可,非常简单。

mysql数据迁移

服务都做好以后,需要把之前服务器mysql里面的数据也迁移过来,可以用django的dumpdata,但是对于数据量比较大的表,容易卡死,io飙得很高,后面采用mysqldump很轻松的就导出数据了,可以先–where指定范围导出,看看快不快,最后在新服务器通过mysql -umysql -pmysql 数据库名 < 导出的数据,这样就可以快速导入了,主要这个数据其实就是一个脚本,它会先删除原来存在的表,再全量导入,如果需要增量导入,需要在导出数据的时候加一个–no_create_info的参数,这样新生成的数据脚本就不会删除原来的表了。

关于docker-compose搭建的mysql-djano-uwsgi-nginx环境,已经开源到github,可以参考django-mysql-uwsgi-mysql-docker-compose,redis部分后面再补充进来,欢迎star。以上就是优化的大部分过程,有了docker就可以一键部署了,后面再迁移时就方便多了,主要遇到的坑都有提到,如有问题,请在下方小程序留言,必定第一时间解答,如有不对的地方,也欢迎指正。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐