Docker Multi-Stage Builds

Docker 的 multi-stage 技术可以把不同的构建阶段分离开,方便修改和复用,同时还能更好地把缓存层组合在一起,避免重复劳动,还能保证最后运行时的干净纯粹,不会带入前边阶段的副产品。不过我在实际场景中遇到的问题比单纯的构建更加复杂一些——我不仅需要不同的构建阶段(build stage),还需要在不同的环境里构建同一个镜像,比如在本地和在 CI/CD 中。CI/CD 的特殊之处在于在前边的阶段中基本上已经完成了 dependency download, build, package 等一系列阶段,因此直接把 build 的结果拷贝到 Docker context 里执行 Dockerfile 的最后几步就可以了。如果复用一个 Dockerfile,那么基本上要做前边的很多重复工作,如果用两份 Dockerfile,则又会造成配置上的冗余,迟早会出幺蛾子。

为了解决这个问题,我打算使用 Docker 的 multi-stage 来做,本意是把 package 之后的几步合并到一个 stage 里,在前边的 build 阶段来区分不同的环境。就像下边的配置一样,设想中这个流程非常完美,Docker 可以根据 ENVIRONMENT 来区分不同的环境,并且在最后的 runtime 阶段中从不同的环境构建阶段中把二进制内容拷贝出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
FROM ubuntu:22.04 AS build-base
FROM ubuntu:22.04 AS runtime-base

# 当前环境
ARG ENVIRONMENT=dev

# 本地构建阶段
FROM build-base AS build-dev
RUN build
RUN package
RUN cp . /app

# CI/CD 构建阶段
FROM build-base AS build-prod
COPY . /app

# 最终阶段
FROM runtime-base AS runtime
COPY --from=build-${ENVIRONMENT} /app /app
ENTRYPOINT [ "/app/main" ]

但是这样做有个非常大的问题就是 Docker 的 build arguments 是只在单个阶段之内管用的,因此倒数第二行的 COPY 压根不认识 ENVIRONMENT 这个变量,进而这个 Dockerfile 根本无法正常构建。找了很多解决方案,最终看到虽然在 Docker 某一个阶段内不认识其他阶段的 build arguments,但是在定义阶段本身的时候是可以复用的,也就是 FROM runtime-base AS runtime 这一行,在这里是可以用 ENVIRONMENT 这个变量的。因此思路就很清晰了,可以直接改成下边这种情形:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
FROM ubuntu:22.04 AS build-base
FROM ubuntu:22.04 AS runtime-base

# 当前环境
ARG ENVIRONMENT=dev

# 本地构建阶段
FROM build-base AS build-dev
RUN build
RUN package
RUN cp . /app

# CI/CD 构建阶段
FROM build-base AS build-prod
COPY . /app

# 最终阶段
FROM build-${ENVIRONMENT} AS runtime
ENTRYPOINT [ "/app/main" ]

但这样有个最大的问题,就是最后的 runtime 阶段直接继承前边 build 阶段,会把这些阶段运行过程中的副产品、垃圾文件和系统包带到最后,这显然极大地违背了 multi-stage 的初衷。继承还是要继承的,关键是不能从 build 阶段继承,那么自然而然地就想到再加上一层干净的 ready 阶段来过渡,保证 ready 阶段中的内容是纯粹的二进制,每个环境都有自己对应的 ready 阶段,然后分别把自己的二进制拷贝进去就行,如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
FROM ubuntu:22.04 AS build-base
FROM ubuntu:22.04 AS runtime-base

# 当前环境
ARG ENVIRONMENT=dev

# 本地构建阶段
FROM build-base AS build-dev
RUN build
RUN package
RUN cp . /app

# CI/CD 构建阶段
FROM build-base AS build-prod
COPY . /app

# 本地准备阶段
FROM runtime-base AS ready-dev
COPY --from=build-dev /app /app

# CI/CD 准备阶段
FROM runtime-base AS ready-prod
COPY --from=build-prod /app /app

# 最终阶段
FROM ready-${ENVIRONMENT} AS runtime
ENTRYPOINT [ "/app/main" ]

这样就可以完美地解决上边的所有问题,除了多出两个几乎一样的 build stages 以外没有什么成本。只要在执行 docker build 的时候传进入对应的 ENVIRONMENT 参数就行。还要注意的是,一定要使用 Docker Buildkit 来执行,这样才会根据最后的 runtime 阶段有挑选地执行需要的阶段,否则 Docker 会傻乎乎地执行所有的阶段,这样就得不偿失了。

Technical Stacks

WebServer / Reverser Proxy

Available Candidates

Name License Extensible Reverse Proxy Load Balance Auto SSL Cloud Native
httpd Apache
Nginx Nginx License Y Y Y
Caddy Apache Y Y Y Y
Traefik MIT Y Y Y
Envoy Apache Y Y

Application Monitors

Available Candidates

Name License Speciality
Zipkin Apache 2.0 Performance monitoring
Elastic APM Apache 2.0 Performance monitoring
Geneva
Geneva

JMeter 使用笔记

安装配置

安装配置

需要安装Java,尽量使用Java SE 8以上的版本。添加把JRE和JDK添加到环境变量中。

JMeter安装

JMeter官网下载相关版本的JMeter链接,直接解压即可。注意要把JMeter的根目录添加到环境变量JMETER_HOME中,之后集群测试会使用到。

单机测试

单机测试主要分为两个大的步骤,一个是添加线程组和Http请求,另一个就是添加Listener来查看测试结果,网络上有很多现成的教程,这里就不再赘述。

集群测试

集群测试很简单,只要把Slave机器部署好,Master会把测试脚本发送给Slave进行测试,部署过程也可以参考这篇文章。具体部署步骤如下:

  1. 在每台Slave上都安装配置好单机环境

  2. 在Slave上修改$JMETER_HOME/bin/jmeter.properties文件,主要修改以下几部分(1099为JMeter服务器默认端口)

    1
    2
    3
    server_port=1099
    server.rmi.localport=1099
    server.rmi.ssl.disable=true
  3. 在Master上修改$JMETER_HOME/bin/jmeter.properties文件,主要修改以下几部分,如果想要在Master上也运行测试程序的话,可以把以上Slave的配置也加到Master里,并且在remote_hosts中添加localhost:1099的项

    1
    2
    # 10.0.0.X为Slave的IP
    remote_hosts=10.0.0.1:1099,10.0.0.2:1099

完成了以上工作,在Master上启动测试程序(运行->远程启动所有)即可开始集群测试。这种模式下有一个非常大的缺点,就是各个Slave的测试结果会实时发送到Master上,对Master的网络和CPU都造成了很大的压力,如果能够积累一定的数据之后批量发送会好很多。也可能是我没有找到正确的使用方法,如果有解决方案,还请不吝告知。

常见问题

Http Post请求该怎么发

Post请求的Body在编程语言层面有很多的叫法,什么Payload、Data、Params、KWargs之类,在JMeter的Http请求界面,选中Method为POST之后,下边的参数栏有三个Tab,分别是Parameters、Body Data以及Files Upload,因为在GET请求时是使用的Parameters,所以这里一上来就往Body Data里填数据。但是当Content-Type为multipart/form-data,并且同时发送参数和文件的时候,在Body Data里填写的参数会被Files Upload的内容覆盖掉。所以至少在这种情况下需要把请求体中的参数填到Parameters栏里。

在其他Content-Type中尚未见到这种限制,比如application/json时就可以直接把Json对象填入到Body Data中。

表单请求无法正常发送

当Content-Type为multipart/form-data,并且同时发送参数和文件的时候,可能请求无法成功发出去,如果其他工具(比如Postman)可以正常发送请求,那么应该是JMeter实现的问题,在Http请求的Advanced标签中把Client implementation换成Java即可解决这个问题。

集群运行时报找不到rmi_keystore.jks错误

因为JMeter集群在运行时默认会开启SSL,所以需要额外进行SSL的配置,如果想要跳过SSL直接使用,需要在jmeter.properties中设置以下内容

1
server.rmi.ssl.disable=true

关于计算机学习

我认为计算机领域真正的学习应该是“博客式”的,即遵循“遇到问题->查阅资料->弄懂问题->有成就感->总结记录”,早些时候我把这个叫做“需求驱动学习。

举个例子

嘟嘟(我家泰迪)平日里自诩Java小王子,无论是手写Runnable还是一口气1 << 8个线程池都信手拈来。有天老板(我)在微信上说:“大家都说‘人生苦短,我用Python’,蟒蛇听起来比咖啡厉害多了嘛,你,赶紧用Python把后台重写一遍”,嘟嘟一边暗自庆幸老板还没听说过PHP,一边嘀嘀咕咕开始了改造之路。在迁移Java的多线程部分的时候,嘟嘟想用Python的Thread来做,但是发现Python中有万恶的GIL(Gay In Love Global Interpreter Lock),想要实现走位酷炫的线程池的计划泡汤了。

一些社区建议使用多进程来代替多线程,但是嘟嘟在写了两个Demo之后发现它们和多线程不一样,变量竟然不能共享,这代码还怎么写,于是继续面向百度编程,在误点进去十几个培训机构的主页之后,嘟嘟终于找到了一个新奇的解决方案——异步编程。在短暂纠结于yield和yield from的写法之后,嘟嘟又找到了更好用的async/await,并顺利把Java上的多任务移植到了Python上。好景不长,有天嘟嘟不小心让视频转码的任务读入了硬盘深处700多个G的马克思主义视频教程,发现Python的其他多任务都不能正常响应了。在妙峰山烧了七八柱香之后,嘟嘟才了解到原来是因为一个异步任务被阻塞住了,导致很多其他任务不能被处理。最终嘟嘟还是把计算密集的任务扔到了多进程上去做。

在整个项目迁移结束之后,嘟嘟开始对迁移过程进行复盘,发现以下几点需要搞明白:

  1. 为啥多进程变量不能共享
  2. 为啥有GIL在多线程就不好用了
  3. 为啥一个异步任务阻塞会影响其他任务
  4. 为啥在百度搜Python老蹦出来培训机构

于是嘟嘟开始了新一轮的调查研究,在经历过以往的教训之后,嘟嘟学会了在一个404的网站上搜索404的信息。这才明白了进程和线程的关系,明白了进程如何通过消息队列进行通信,明白了异步编程的好处和局限性以及事件循环的原理。无论是进程、线程、协程还是纤程,本质都是想要达到一个目的,即“在需要的时候占用CPU,不需要的时候释放CPU”。找了一堆的资料之后,嘟嘟又打开了大学时崭新的操作系统课本,把处理器这一章从头读了一遍,发现醍醐灌顶,每一句都是好东西,感叹自己当时上课怎么就没发现这本书的精妙之处。感慨之余,决定把自己的感受记录下来,标题就叫《关于计算机学习》。

我的经历

从本科一年级第一节编程课开始,我就喜欢上了编程。之后整个一年级都沉浸在ACM刷题和囫囵吞枣的学习之中。虽然当时只会命令行编程,但是还是做出了一些小玩意,比如自动计算游戏中交易的收益、收集名侦探柯南TV版的分集信息等,当时没什么备份的概念,也不懂版本控制,现在程序都遗失了,留下来的只有一个基于MFC的计算器。

我的专业是计算机科学与技术,当时要学习一些计算机的基础课,比如操作系统、数据结构、计算机组成、计算机网络、编译原理、数据库等,在大学前两年中,我一直都特别讨厌这些科目。一边是自由新奇的编程实践,另一边是枯燥和看似无用的琐碎知识点——就像高中一样,显然后者无法引起任何人的兴趣——即使有,在一个具有强实践性质的专业中谈纯粹在课本中获得的快乐也和耍流氓无异。当时在这些课程中我相对不那么讨厌的是数据结构课,因为其中的很多算法我在很久之前就已经在POJ上刷过很多次了,所以上课的时候有一种仅通过预习无法感受到的亲切感——这也是本文想要传达的观点。

我是什么时候有了“还是制定专业课计划的那几个老头厉害,是我当时太年轻了”这种想法呢?在大二结束和大三开始这段时间,随着写代码越来越多,接触的领域越来越多,我开始做了一个在当时看来算得上是巨无霸的项目,从前端到后端都是我一个人完成。和我之前接触的项目不同,这个项目是真的有很多用户的(笑),所以系统上线之后不断暴露出越来越多的问题,比如数据库查询很慢,比如网络延迟很高,比如客户端卡顿等。在给自己收拾烂摊子的时候,开始重新学习了多线程、数据库、计算机网络(主要是退避算法之类),然后猛然惊觉,“这不就是我大学里的专业课么?”。

时间一晃到了现在,我已经研二了。在给师弟师妹们介绍我那点不成器的经验时,我的观点也从“多实践多编程”转变成了“先把基础打好”。计算机科学与技术的专业课都很重要,无论讲课的老师水平如何,都一定要学好,它们是构造整个互联网空间的基向量。话虽如此,这并不代表我完全同意目前计算机教学的思路,我认为计算机领域真正的学习应该是“博客式”的,即遵循“遇到问题->查阅资料->弄懂问题->有成就感->总结记录”,早些时候我把这个叫做“需求驱动学习”。

我的观点

为什么我们都不爱听大道理?为什么我们听了那么多道理仍然过不好这一生?为什么我们反感鸡汤?

我认为计算机的这些基础课就像所谓的大道理一样,没有相关的经历作为培养基的话是无论如何也不可能理解的,自然只能觉出枯燥无味和腐朽陈旧来。但是倘若踩过了无数的坑就会明白,这些基础课本字字珠玑,毫不过时(也可见我们的科学发展其实并没有大家想的那么快),古人诚不我欺也(有多少人都写成“诚不欺我”,意思还是一个意思,但是对话场景瞬间从项脊轩蹦到了王老大烧烤摊)。想要理解多少大道理就要踩多少坑,该踩的坑一个都少不了。

不要误会,我不是在宣扬基础教学无用论,我的意思是初学者一开始不必过于深入地了解基础知识,因此此时无法真正理解,不如先拓宽知识面,暂时了解有这么回事就可以,把一部分的时间匀出来自己去折腾,只要智力正常并且适合干这行,很快就会产生深入学习的需求的,这时候的学习效率远比按部就班划拉书本要高很多。以嘟嘟来举例,学习处理器调度的过程可以分为以下几个阶段:

  1. 了解到一切计算都被分解成指令交给处理器顺序执行
  2. 在Bilibili上自动监测赶海四天王的视频,并及时下载
  3. 查阅类似实现的开源代码,学习,重复踩坑,完成需求
  4. 查阅课本或工具书,学习进程和线程的原理、关系以及区别
  5. 拓展了解协程、纤程、Actor等异步编程模式
  6. 接触NodeJS、Python、C#、GoLang在多任务上的做法,对比学习
  7. 感觉自己很厉害,写博客交流学习,一写笔才发现还有很多细节不懂,继续学习
  8. 开始踩下一个坑……

以上只是一个捏造的例子,用来说明一个渐进式的学习过程可能是什么样子的,实际过程中的步骤或许没有这么精细和繁琐,但大步骤不会差很多。我可以保证相当程度的低年级计算机专业同学对于以上的这些东西都没有清晰的概念,所有东西都糊作一团。根据我浅薄的经历来看,有一些经验比较丰富的同学可能只是停留在前三个阶段,实事求是地讲,代码风格和注释都很漂亮,但是就是无法再往前一步。顺便一提,还有一些同学在学习某些语言或者框架时,总是会虔诚地走完“买书->找视频->进技术群”这个流程,我认为这样效率是比较低的,有那百度云下载视频的时间官网文档都看了好几遍了。不如先找个点切入,即使是Hello World,然后一步步拓宽把整个需求盘下来,在复盘的时候再通过书或者是视频系统学习。系统学习应该是后置的,连它能干嘛都还不知道,系统学习又有什么用呢,结果只能系统地遗忘。至于技术群,其作用是收表情包,和技术没啥关系。

无论需求从哪里来,是随便玩玩,还是饭圈妹子的抢票委托,还是老板或外包的要求,只要你决定实现一个需求,下一步就是分析和调查需求应该怎么实现。如果你有一定的基础知识(即使是广泛而不深入的),那么调研的过程会更有针对性,接下来就开始”Done is better than perfect”的过程,其间随着踩坑会开始接触相关领域的知识,然后拓展学习总结出一套敝帚自珍的宝贝,在成就感和虚荣心的驱使下,把这些碎碎念记录下来,记录的过程发现原来还有很多细节自己根本没弄清楚,再去迭代学习,一步步把这篇博客写完。基础知识在这个过程中会一步步得到加强,每一次都是重新认识,每一次的认识都更加清晰几分。

或许你又会问,总是实现一个又一个大同小异的需求,如何才能摆脱成为CRUD Boy的命运呢?多想,多拓展,多总结记录,并乐在其中,It’s that simple.

在服务器上搭建 Jupyter Notebook

Jupyter Notebook

安装Jupyter

假定工作目录为/home/jupyter

1
2
3
$ virtualenv venv -p python3
$ source venv/bin/activate
$ (venv) pip install jupyter

配置Jupyter

安装Jupyter之后,在~/.jupyter下查看是否存在jupyter_notebook_config.py文件,如果没有,就使用

1
$ (venv) jupyter notebook --generate-config

命令生成配置文件,Jupyter的具体配置内容参见Jupyter Notebook的配置选项,下边的几个选项是为部署在服务器上可能要用到的(下边c.NotebookAPP.password的设置方法见Jupyter Notebook添加密码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Nginx访问时会出现跨域访问,需要在这里允许
c.NotebookApp.allow_origin = '*'
# 禁止随意修改密码
c.NotebookApp.allow_password_change = False
# 是否允许远程访问
c.NotebookApp.allow_remote_access = True
# 访问URL,假定我们想通过`$HOST/python`来访问
c.NotebookApp.base_url = '/python'
# 访问之后跳转的URL(自定义),要加上base_url
c.NotebookApp.default_url = '/python/tree'
# Jupyter Notebook Server监听的IP
c.NotebookApp.ip = '127.0.0.1'
# Jupyter Notebook的工作目录,用于限制访问位置
c.NotebookApp.notebook_dir = 'data/'
# 启动Jupyter Notebook之后是否打开浏览器(服务器上此选项应该关闭)
c.NotebookApp.open_browser = False
# 客户端打开Jupyter Notebook的密码哈希值
c.NotebookApp.password = 'sha1:******'
# Jupyter Notebook Server监听的端口
c.NotebookApp.port = 8888

集成 Nginx

Jupyter Notebook使用tornado作为服务器和Web框架,如果想要获取更高的性能以及灵活性,可以使用Nginx作为代理服务器。在/etc/nginx/conf.d/jupyter.conf中添加以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
server {
listen 80 default_server;
server_name myjupyter.com;
charset utf-8;
client_max_body_size 75M;

location /python/ {
# 这里要和Jupyter配置中的Base Url一致
proxy_pass http://127.0.0.1:8888/python/;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
# 因为用到了Websocket协议,所以下边的配置是必须的
proxy_set_header Connection "upgrade";
proxy_redirect off;
}
}

配置完成之后,启动Jupyter Notebook即可远程访问

1
2
3
4
# 直接运行,测试使用
$ (venv) jupyter notebook
# 后台运行
$ (venv) nohup jupyter notebook &

此时在浏览器输入http://myjupyter.com/python即可进入Jupyter Notebook。

Python异步编程

异步编程

在Python中,由于CPythonGIL限制,不能使用多线程充分利用资源,因此在进行诸如文件存取、网络请求等IO操作的时候极其浪费资源,程序往往要在一个点上空等。虽然Python可以借助多进程来改善,但是进程相比线程来说过重,如果只用多进程就可以完全解决问题,线程这个概念也就不会出现了。

在耗时任务(主要是IO)的操作上,Python提供了一些方法来解决,比如协程的概念。初次了解协程的时候认为这是可以拯救世界的东西,概念新颖,方法独特,但是在了解了如C#的async/await以及Javascript(同样是单线程语言)的async/await之后,明白了基于yield/send的协程仍然使用起来仍然不够顺手。

Python3.4版本之后,Python引入了asyncio标准库,使用@coroutineEventLoop可以更方便地完成Python协程的工作,接着在Python3.5中,Python引入了async/await关键字,彻底简化了Python异步编程流程。自从C#最早提出async/await之后,很多语言都引入了这一机制,因为它真的太好用了,可以完全以同步的风格进行异步编程。不过由于与C#的实现机制不同,在Python协程中仍然有一些具体的细节无法回避,比如send/yield的交互。

工具对比

在Python生态系统中,有很多异步编程库可以使用,这些编程库有的是诞生于Python异步支持之前,自行实现了事件循环,有的是依托于Python的异步机制进一步开发,在进行异步编程的时候,可以借助以下的工具来简化工作:

  • gevent:自己实现了时间循环,很早的异步处理库
  • twisted:非常早的异步网络库,自带HTTP服务器、DNS服务器、邮件服务器等,之后Python官方的asyncio就很大地收到了它的影响
  • tornado:既是异步网络编程库,也是一个成熟的HTTP服务器以及Web框架
  • aiohttp:基于asyncio的网络编程库,可以高效地实现HTTP服务器和Websocket服务器
  • cyclone:作者想要综合twistedtornado两个库,做到implements the Tornado API as a Twisted protocol
MySQL 单机单表切分实践

MySQL 单机单表切分实践

描述

客户的项目使用MySQL做持久化,MySQL部署在单机服务器上,前期在数据存取上没有问题。后来加了一个爬虫项目,爬取百度地图的数据,数据很快堆到了一亿多条,所有的数据都存储在单个的MySQL数据表中,整体的数据量超过了70GB,查询时的效率极低,几分钟才能出来结果。除此之外,前期分配的磁盘空间不足,整体的数据占用量也到了95%以上。所以一方面需要迁移MySQL的存储位置,另一方面需要解决查询效率的问题。

过程

存储迁移

在解决线上问题的时候,我的宗旨一直是尽量别相信中文社区的解决方案(包括本文),不过在做数据迁移的时候图省事直接找了个CSDN照做了,过程都是泪,最后还是老老实实照着StackOverflow做,迁移MySQL存储位置的方案看这里,简要描述如下:

  1. 假设你的迁移目标目录是/data/mysql
  2. 假设你的MySQL配置文件的目录是/etc/mysql/mysql.conf.d/mysqld.conf
1
2
3
4
5
6
1. $ sudo /etc/init.d/mysql stop # 或 sudo service mysql stop
2. $ sudo cp -R -p /var/lib/mysql /data/mysql
3. 打开/etc/mysql/mysql.conf.d/mysqld.conf, 将datadir指向/data/mysql
4. 打开/etc/apparmor.d/usr.sbin.mysqld,将其中所有的/var/lib/mysql修改为/data/mysql
5. $ sudo /etc/init.d/apparmor reload
6. $ sudo /etc/init.d/mysql restart # 或 sudo service mysql start

按照上述步骤就可以顺利完成存储的迁移,如果期间确实遇到了问题,那么就删除存储目录下的ib_logfile0ib_logfile1这两个文件,重新启动MySQL。

查询优化

优化查询的第一个反应就是加索引,查询依据主要是一个varchar的列,所以最初考虑直接对这一列加索引,设置了索引之后一直等它运行完成,结果一直做了四个多小时仍然没有结束。由于这个尝试早于存储迁移,而且加索引的过程中会产生大量的临时文件,所以直接撑爆了磁盘,搞了很久才救回来。也是由于数据量很大的原因,没有做备份就直接怼了索引,现在想起来也是大胆。这个尝试之后就加了块大磁盘,先做好了存储迁移,然后开始考虑单表切分的问题。

就现在的用户量而言,主要的压力并不在服务器本身,所以仍然考虑单机切分。数据表的字段之间没有特别强的关联,而且有几个字段的内容量很大,可是客户端需要的字段比较多,如果做垂直切分最后还是要Join,因此最后做了表的水平切分。客户端在查询的时候总是会带一个地区参数,而且参数只是城市,可以根据区域做水平切分。如果按照省份做切分,理想状态下会把数据表均匀切分成30多份,按照目前的数据增长速度,估计几个月之后又会上升到现在的量级,所以干脆按照城市进行切分,并且这次直接在新表上加索引。

在准备阶段,给数据表一个统一的前缀,结尾加上城市的Canton Id,用代码批量生成Model类,然后Migrate即可(项目基于Django)。接下来就是切分过程,大致思路是按照id每次从旧表中捞出10000条数据,根据city字段判断应该插入的新表,放在临时列表中,然后批量插入整个临时列表。在做切分的过程中还是遇到了一点小坑,首先是Django的查询集缓存问题,规范可以参考官方文档,做的时候有这个意识,但是还是没有足够细心,导致一开始速度慢了很多。另外还有一个更慢的地方,是在拼装新的Model实例的时候,这个过程理论上应该一瞬间完成,可是却成了时间瓶颈,检查了很久发现是一句item.city.canton_id导致了每次都重新查询一次数据库,做了City表中id到canton_id的映射之后这个问题才得以解决。外键写起来是个好东西,可是用起来稍不注意就忘了其凶残的本质,以后尽量不设置外键而是自己维护关联关系,这样才能时刻记住自己在做什么

结果

截至目前,迁移工作仍然在进行中,做完之后再来补…

分布式平台搭建

搭建Hadoop

以下操作皆基于Ubuntu 16.04 32位环境 & Hadoop 2.7.3版本

单节点环境

  • 首先安装相关的准备环境

    1
    2
    3
    4
    $ sudo apt update
    $ sudo apt install -y default-jdk
    $ sudo apt install -y vim
    $ sudo apt install -y openssh-server
  • 配置JAVA_HOME

    • 一般来说Ubuntu平台上通过default-jdk安装的java位置在/usr/lib/jvm/default-jdk目录
    • /etc/profile中添加下边两行内容并保存退出
      1
      2
      export JAVA_HOME=/usr/lib/jvm/default-java
      export PATH=$PATH:$JAVA_HOME/bin
  • 添加用户和用户组

    1
    2
    3
    $ sudo addgroup hadoop
    $ sudo adduser --ingroup hadoop hduser
    $ sudo usermod -a -G sudo hduser
  • 配置 SSH

    • 切换到hduser,并执行以下操作

      1
      2
      $ ssh-keygen -t rsa -P ""
      $ cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
    • 之后尝试使用下边的命令连接本机,这个操作也会将本机加入到known hosts里

      1
      $ ssh localhost
  • 安装Hadoop

    • 下载hadoop-2.7.3.tar.gz,以下假定工作目录为/home/hduser

      1
      2
      3
      $ tar -zxvf hadoop-2.7.3.tar.gz
      $ mv hadoop-2.7.3/ hadoop/
      $ sudo chown -R hduser:hadoop hadoop
  • 编辑bash配置

    • 打开.bashrc,加入以下内容(也可以修改/etc/profile,但是使用.bashrc更加灵活)

      1
      2
      3
      export JAVA_HOME=/usr/lib/jvm/default-java
      export HADOOP_HOME=/home/hduser/hadoop
      export PATH=$PATH:$JAVA_HOME/bin:$HADOOP_HOME/bin
  • 配置Hadoop(不同版本的hadoop的配置方案会有不同,配置之前记得要确定版本)

    • 修改~/hadoop/etc/hadoop/hadoop-env.sh,在文件中添加下边的内容

      1
      export JAVA_HOME=/usr/lib/jvm/default-java
    • 修改~/hadoop/etc/hadoop/yarn-env.sh,在文件中添加下边的内容

      1
      export JAVA_HOME=/usr/lib/jvm/default-java
    • 修改~/hadoop/etc/hadoop/core-site.xml

      • 执行以下命令

        1
        2
        3
        $ sudo mkdir -p /home/hduser/tmp
        $ sudo chown hduser:hadoop /home/hduser/tmp
        $ sudo chmod 750 /home/hduser/tmp
      • 在core-site.xml添加以下内容(各个属性的内容可以按照实际情况修改)

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        <configuration>
        <property>
        <name>fs.defaultFS</name>
        <value>hdfs://master:9000</value>
        </property>
        <property>
        <name>io.file.buffer.size</name>
        <value>131072</value>
        </property>
        <property>
        <name>hadoop.tmp.dir</name>
        <value>file:/home/hduser/tmp</value>
        <description>Abase for other temporary directories.</description>
        </property>
        <property>
        <name>hadoop.proxyuser.hduser.hosts</name>
        <value>*</value>
        </property>
        <property>
        <name>hadoop.proxyuser.hduser.groups</name>
        <value>*</value>
        </property>
        </configuration>
    • 修改~/hadoop/etc/hadoop/hdfs-site.xml,添加以下内容(各个属性的内容可以按照实际情况修改)

      • 执行以下命令

        1
        2
        3
        4
        5
        6
        $ sudo mkdir -p /home/hduser/dfs/name
        $ sudo chown hduser:hadoop /home/hduser/dfs/name
        $ sudo chmod 750 /home/hduser/dfs/name
        $ sudo mkdir -p /home/hduser/dfs/data
        $ sudo chown hduser:hadoop /home/hduser/dfs/data
        $ sudo chmod 750 /home/hduser/dfs/data
      • 在hdfs-site.xml中添加以下内容

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        <configuration>
        <property>
        <name>dfs.namenode.secondary.http-address</name>
        <value>master:9001</value>
        </property>
        <property>
        <name>dfs.namenode.name.dir</name>
        <value>file:/home/hduser/dfs/name</value>
        </property>
        <property>
        <name>dfs.datanode.data.dir</name>
        <value>file:/home/hduser/dfs/data</value>
        </property>
        <property>
        <name>dfs.replication</name>
        <value>3</value>
        </property>
        <property>
        <name>dfs.webhdfs.enabled</name>
        <value>true</value>
        </property>
        </configuration>
    • 修改~/hadoop/etc/hadoop/mapred-site.xml,添加以下内容(各个属性的内容可以按照实际情况修改)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      <configuration>
      <property>
      <name>mapreduce.framework.name</name>
      <value>yarn</value>
      </property>
      <property>
      <name>mapreduce.jobhistory.address</name>
      <value>master:10020</value>
      </property>
      <property>
      <name>mapreduce.jobhistory.webapp.address</name>
      <value>master:19888</value>
      </property>
      </configuration>
    • 修改~/hadoop/etc/hadoop/yarn-site.xml(各个属性的内容可以按照实际情况修改)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      <configuration>
      <property>
      <name>yarn.nodemanager.aux-services</name>
      <value>mapreduce_shuffle</value>
      </property>
      <property>
      <name>yarn.nodemanager.aux-services.mapreduce.shuffle.class</name>
      <value>org.apache.hadoop.mapred.ShuffleHandler</value>
      </property>
      <property>
      <name>yarn.resourcemanager.address</name>
      <value> master:8032</value>
      </property>
      <property>
      <name>yarn.resourcemanager.scheduler.address</name>
      <value> master:8030</value>
      </property>
      <property>
      <name>yarn.resourcemanager.resource-tracker.address</name>
      <value> master:8031</value>
      </property>
      <property>
      <name>yarn.resourcemanager.admin.address</name>
      <value> master:8033</value>
      </property>
      <property>
      <name>yarn.resourcemanager.webapp.address</name>
      <value> master:8088</value>
      </property>
      </configuration>
  • 格式化Namenode

    • 进入~/hadoop/bin目录下,执行以下操作

      1
      $ ./hdfs namenode –format
  • 启动hadoop

    • ~/hadoop/sbin执行以下命令

      1
      2
      $ ./start-dfs.sh
      $ ./start-yarn.sh
    • 之后通过jps命令可以看到以下结果,说明启动成功

      1
      2
      3
      4
      13058	Jps
      13026 NodeManager
      12916 ResourceManager
      12169 DataNode

集群环境

此处默认master和slave已经按照单机节点配置完成

  • 设置网络

    • 我的例子是一台master一台slave,分别确定两者的ip,然后在两台机器的/etc/hosts中写入以下内容

      1
      2
      192.168.0.1    master
      192.168.0.2 slave
  • 配置ssh免密登录

    • 在master上执行以下命令

      1
      $ ssh-copy-id -i ~/.ssh/id_rsa.pub hduser@slave
    • 在slave上执行以下命令

      1
      $ ssh-copy-id -i ~/.ssh/id_rsa.pub hduser@master
    • 之后两方互相登录测试一下是否可以免密登录

  • 配置~/hadoop/etc/hadoop/slaves

    • 在master机器的slaves文件中添加以下内容

      1
      2
      master
      slave
  • 格式化Namenode

    • 进入master的~/hadoop/bin目录下,执行以下操作

      1
      $ ./hdfs namenode –format
  • 启动hadoop

    • 在master的~/hadoop/sbin执行以下命令

      1
      2
      $ ./start-dfs.sh
      $ ./start-yarn.sh
    • 之后通过jps命令可以看到以下结果,说明启动成功

      • master中

        1
        2
        3
        4
        5
        6
        4048	Jps
        3899 ResourceManager
        4013 NodeManager
        3726 SecondaryNameNode
        3423 NameNode
        3535 DataNode
      • slave中

        1
        2
        3
        2754	DataNode
        3012 Jps
        2903 NodeManager

搭建Spark

Spark平台

  • 下载Spark

    • 这里下载和hadoop版本对应的spark,假定工作目录为/home/hduser

    • 进行如下操作解压压缩包(每个节点都要进行)

      1
      $ tar -zxvf spark-2.3.1-bin-hadoop2.7.tgz
  • 配置~/.bashrc文件

    • 在.bashrc文件中添加以下内容(每个节点都要添加)

      1
      2
      export SPARK_HOME=/home/hduser/spark
      export PATH=$PATH:$SPARK_HOME/bin
  • 配置~/spark/conf/slaves文件

    • 在两个节点的salves文件中添加以下内容

      1
      2
      master
      slave
  • 启动Spark

    • 在master的~/spark/sbin中执行以下命令

      1
      ./start-all.sh
    • 在master中执行jps,发现以下结果说明spark运行成功

      1
      2
      3
      4
      5
      6
      5257	Worker
      5177 Master
      3726 SecondaryNameNode
      5326 Jps
      3432 NameNode
      3535 DataNode
    • 在slave中执行jsp,发现一下结果说明spark运行成功

      1
      2
      3
      2754	DataNode
      3717 Worker
      3771 Jps

配置Scala

  • 下载Scala

    • 这里相应版本的scala压缩包,假定工作目录为/home/hduser

    • 进行如下操作解压压缩包(只需要在master进行)

      1
      $ tar -zxvf scala-2.12.6.tgz
  • 配置~/.bashrc文件

    • 在.bashrc文件中添加以下内容(只需要在master在master上执行以下命令)

      1
      2
      export SCALA_HOME=/home/hduser/scala
      export PATH=$PATH:$SCALA_HOME/bin

配置 Eclipse

  • 安装Eclipse

    • 在master上执行以下命令

      1
      $ sudo apt install eclipse-platform
  • 安装Eclipse插件

参考资料

  1. 在Ubuntu上搭建单节点Hadoop
  2. 在Ubuntu上搭建Hadoop集群

Python 'python ImportError: No module named XXX' 问题

最近做毕业设计,用Python写一个文本爬虫,我的目录结构是这样的:

1
2
3
4
5
6
7
StockTextDigger
----src
--------main.py
--------__init__.py
----parser
--------__init__.py
--------source_parser.py

当要从main.py中引用source_parser.py中的一个类时出现了“python ImportError: No module named source_parser”的错误。之前写过的Python从没出现过这样的问题,可是这次无论责骂么都解决不聊,无论是加上 __init__.py 还是sys.path.append(‘XXX’)都没有用,然后忽然意识到parser这个包很有可能已经在默认搜索路径中了,然后改名字,顺利解决。

又一个官逼同的典型例子[摊手]