在用微服务工作时,我们通常还需要在本地进行一些开发,这可能是一个痛点。在本篇文章中,我们将探讨一个可能有效的解决方案。
本篇文章是关于……微服务和Docker,额,还有什么?在这篇文章中,我想举例说明我在工作中最新遇到的问题,问题关于本地环境以及构成基础结构一部分的全部微服务。
有很多关于微服务的文献,以及适应生产环境来处理问题的挑战。但是,关于在本地环境中使用微服务的开发人员的奋斗成果的文献却并不多见。
在过去,你会用几个星期或几个月来开发一个单一的应用程序。本地环境为运行该应用程序而配置,很少需要更改任何设置。从这个意义上说,微服务的出现改变了这一游戏规则。由于不同的微服务是使用不同的技术开发的,因此有必要为每个服务配置不同的本地环境。因此,如果您需要频繁地从一个服务跳转到另一个服务,则还需要更改本地配置。
我最近亲身体验了这个经历,我被要求给一个“旧”微服务(我的意思是几个月前开发的一个微服务)修复bug。这个服务投产以来,我一直参与其他一些微服务的开发,因此,可以理解的是,当我改变环境来修复该bug时,我感觉有点不知所措。这些问题都浮现在我的脑海中:
这是Java还是Scala应用程序?这项服务如何运行?什么是可部署文件?它有哪些依赖关系:数据库,消息代理,其他服务,AWS S3上的存储?连接到这些系统的凭证是什么?需要设置哪些环境变量?一句话,我怎样能够在我的本地环境中运行此服务,以便我可以尝试复现bug并修复它?
随着时间的推移,本地环境变得乱七八糟,设置改变,软件升级,结果,旧的服务可能在忽略它几周后就不准备运行了。我相信这对于需要在不同服务中频繁切换环境的人来说很熟悉。
此外,应用程序在本地运行的方式通常与在生产环境上执行应用程序的方式不同。在本地环境中,应用程序是在IDE或是像Maven、sbt等构建工具上运行的,然而在生产环境上部署的是可执行文件(例如 jar文件)。应用程序运行方式上的这种差异可能会产生严重后果。
另一个潜在的问题是使用“localhost”作为主机名。尽管方便,但不同的服务在用 "localhost" 相互通信,掩盖了"通过网络"通信的复杂性。说实在的,我看到一些新手想知道为什么他们部署配置侦听“localhost”之后无法访问应用程序。
为了解决“localhost”问题,我发现Docker非常有用。
虽然看起来似乎并不明显,在Docker上配置微服务的第一个优势是,配置在Docker上运行的服务构建了关于其本身的优秀文档。在很多情况下,我最后直接在笔记本电脑上运行该服务,但如果对如何执行操作有疑问,我只需要看看Dockerfile或Docker Compose脚本。
在第二个优势是Docker容器的加速能力,瞧,它所有依赖服务都准备就绪了。
我通常使用混合方法,直接在我的笔记本电脑上运行服务,并在Docker容器中运行所有依赖项(数据库,消息代理等)。这样,第三个优势,我可以迅速地重新创建数据库并修改它们,而且不用担心会破坏任何东西。
此外,第四个优势,“dockerizing”服务可以轻松地准确地反映生产环境。例如,在某些情况下,我遇到了一个与数据库相关的bug,我无法在本地环境中重现该错误。数据库是MySQL,花了很多时间让我的本地数据库进入类似于生产环境的状态之后,这个bug仍然难以捉摸。最后,我设法找到了问题所在:我本地的MySQL安装版本比生产环境的更新,因此系统变量的缺省值explicit_defaults_for_timestamp是不同的。我一注意到这一点,就在一个Docker容器中设置了一个MySQL服务器,其配置与生产环境相同,这个bug就浮出水面了。bug重现使人心安,我很快就可以修复它。
关于这个话题的另一个真实案例是使用Swagger时遇到的一个bug。问题在于Swagger UI 的“Model|Model Schema”部分没有显示在Staging环境上,而是出现在我的本地环境中。我的初步调查认为这问题与环境有关的。然而,真正的问题竟然是Json依赖关系中的一个冲突。这个问题没有在本地环境中显露,因为在我的笔记本电脑中,我通常使用IDE或“sbt”来运行我的应用程序。但是,Staging环境上的应用程序,是通过执行sbt-assembly的fat jar插件打包的jar包来运行的。这个插件非常棒,但它可能会导致一些棘手的情况,因为它会提取所有jar打包到一个ja包r中,因此会删除各个jar名称提供的名称空间保护。我能找到症结所在是因为在Docker容器中运行应用程序,就像在Staging环境和生产环境上运行一样。显然,我可以通过使用fat jar运行应用程序来在本地重现它(但要做到这一点,我需要知道我正在调查的问题的原因!)
现在我们来看一个例子。让我们来想象一家虚构的航空公司,EasyRide。EasyRide的系统包含以下服务:
我们假设:
考虑到以上这些,Docker Compose脚本将如下所示
version: '2'
services:
redis:
image: redis:latest
ports:
- "6379"
volumes:
- ./volumes/redis:/data
mysql_checkout:
build: ./mysql
ports:
- "3306:3306"
volumes:
- ./volumes/mysql_checkout:/var/lib/mysql
- ./volumes/mysql_conf:/etc/mysql/conf.d
environment:
- MYSQL_ROOT_PASSWORD=admin
- MYSQL_DATABASE=checkout
- MYSQL_USER=admin
- MYSQL_PASSWORD=admin
mysql_tickets:
build: ./mysql
ports:
- "3307:3306"
volumes:
- ./volumes/mysql_tickets:/var/lib/mysql
- ./volumes/mysql_conf:/etc/mysql/conf.d
environment:
- MYSQL_ROOT_PASSWORD=admin
- MYSQL_DATABASE=tickets
- MYSQL_USER=admin
- MYSQL_PASSWORD=admin
activemq:
image: rmohr/activemq:5.13.1
ports:
- "61616:61616"
- "8161:8161"
volumes:
- ./volumes/activemq:/var/activemq/data
search:
build: ./search
ports:
- "8081:8080"
- "9010:9010"
- "5005:5005"
links:
- redis
environment:
THIRDPARTY_HOST1: http://10.200.10.1:9002
REDIS_HOST: redis
REDIS_PORT: 6379
DOCKER_IP: 192.168.99.100
RMI_PORT: 9010
DEBUG_PORT: 5005
APP_ARTIFACT: search-assembly-1.0.1.jar
checkout:
build: ./checkout
ports:
- "8082:8080"
- "9011:9010"
- "5006:5005"
links:
- mysql_checkout
environment:
THIRDPARTY_HOST2: http://10.200.10.1:9002
MYSQL_HOST: mysql_checkout
MYSQL_USER: admin
DOCKER_IP: 192.168.99.100
RMI_PORT: 9010
DEBUG_PORT: 5005
APP_ARTIFACT: checkout-assembly-1.0.0.jar
tickets:
build: ./tickets
ports:
- "8083:8080"
- "9012:9010"
- "5007:5005"
links:
- mysql_tickets
- activemq
environment:
THIRDPARTY_HOST3: http://10.200.10.1:9002
DOCKER_IP: 192.168.99.100
RMI_PORT: 9010
DEBUG_PORT: 5005
AWS_ACCESS_KEY: ABCD49DRF02MD5JDK
AWS_SECRET_KEY: ed7JKKmmK4DNjj32kDJH
S3_REGION: eu-west-1
MYSQL_HOST: mysql_tickets
MYSQL_USER: admin
ACTIVEMQ_PRIMARY_HOST: activemq
APP_ARTIFACT: tickets.jar
exchanges:
build: ./exchanges
ports:
- "8084:8080"
- "9013:9010"
- "5008:5005"
links:
- tickets
environment:
THIRDPARTY_HOST4: http://10.200.10.1:9002
TICKET_HOST: tickets
TICKET_PORT: 8080
DOCKER_IP: 192.168.99.100
RMI_PORT: 9010
DEBUG_PORT: 5005
“redis”服务和“activemq”服务是由镜像直接构建的,而其余的则是由Dockerfile构建的。
MySQL数据库由两个不同的服务'mysql_checkout'和'mysql_tickets'表现。推荐的方法是“checkout”和“tickets”使用相同的数据库,而不是同时拥有两个服务。
''redis,'activemq','mysql_checkout'和'mysql_tickets'这四个服务的量卷被映射到一个本地文件夹,这样即使在停止/移除Docker容器之后,这些卷中存储的任何数据都会被持久化。因此,如果由于任何原因需要重新创建其中一项服务,则新生成的服务将使用本地文件夹中存在的数据进行调配。说到配置数据库,MySQL数据库的模式是由Liquibase管理的脚本创建的。值得注意的是,如果我们想将两个MySQL数据库都暴露给外部,那么就有必要使用不同的端口。在上面的脚本中,“mysql_checkout”和“mysql_tickets”分别暴露在端口3306和3307上。
同样,“search”,“checkout”, “tickets”,和“exchanges”,这四个服务都分别暴露在8081到8084这四个不同的端口上。
用于创建MySQL服务的Dockerfile是:
FROM mysql:latest
RUN deluser mysql
RUN useradd mysql
所以这基本上只是重新创建用户“ mysql”的原始镜像,是在我的Mac上解决此问题所必需的。
我还将我的本地文件夹“./volumes/mysql_conf”映射到容器的文件夹“/etc/mysql/conf.d”,以便配置与生产环境配置相同的MySQL服务器(正如我前面讨论过的关于属性explicit_defaults_for_timestamp的问题)。
Java服务的Dockerfile是
FROM openjdk:8-jdk
WORKDIR /app
COPY $APP_ARTIFACT .
COPY entrypoint.sh .
RUN chmod +x ./entrypoint.sh
EXPOSE 8080 9010 5005
ENTRYPOINT ["./entrypoint.sh"]
以及“entrypoint.sh:”的内容
#!/bin/bash
java \
-Dcom.sun.management.jmxremote \
-Dcom.sun.management.jmxremote.port=$RMI_PORT \
-Dcom.sun.management.jmxremote.local.only=false \
-Dcom.sun.management.jmxremote.authenticate=false \
-Dcom.sun.management.jmxremote.ssl=false \
-Djava.rmi.server.hostname=$DOCKER_IP \
-Dcom.sun.management.jmxremote.rmi.port=$RMI_PORT \
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$DEBUG_PORT \
-jar $APP_ARTIFACT
上面的代码片段显示了带有标志的Java命令,以便通过RMI启用远程JMX并使用JDWP进行远程调试。这将允许诸如JConsole或Visual VM之类的工具连接到在Docker上运行服务以及IDE以调试上述服务。必须将属性java.rmi.server.hostname设置为我Docker虚拟机的外部可访问IP地址(如果未明确设置,RMI服务器将公开Docker指定的内部IP地址)。
Docker Compose文件还包含一些类型环境变量:
THIRDPARTY_HOST<n>
这些变量表示与第三方服务的外部依赖关系。在脚本中,它们都具有相同的值,http://10.200.10.1:9002。该URL对应于我的WireMock服务器监听的本地网络接口。我不想依赖外部服务的可用性来运行我的服务,这就是为什么我有一个本地服务器来支持这些外部服务。本地服务器被配置为服务于不同类型的响应,并允许我模拟多种不同的情况。顺便说一下,我也可以在Docker容器上进行设置,但我更愿意直接在笔记本电脑上运行它,以便快速进行更改。为了在Docker容器上运行的服务能够命中我的WireMock 服务器,我需要用命令为我的Mac分配一个IP
sudo ifconfig lo0 alias 10.200.10.1/24
我希望这篇文章能够帮助那些所有努力让微服务在本地环境中保持稳定的人。以这种方式使用Docker,如果仅仅作为一种记录如何运行微服务的方式,是非常有用的,并且使服务之间的切换环境变得简单。此外,能够随意重新创建数据库、消息代理等并生成生产环境配置镜像也是不可否认的优势。