一个微服务架构的简单示例

最近,在学习微服务架构,看了很多相关的资料,可一直都没有真正动手操作。所以今天,我创建了一个简单的web应用程序示例,让我们通过这个例子来更好地感受微服务的系统架构魅力。这款应用程序做的非常简单:提供一批网上招聘的URL,我们的Web应用就能找到工作描述的文字,并生成一个Word Cloud(词云:许多特定意义的词)。在某些特定的职位招聘中,能够掌握专业技能或流行词汇对HR的人员来说是非常有用的。

微服务应该是独立的、无状态的应用程序,每个应用程序都只关注于某件小事。在这个示例的应用程序中,有以下几个任务:

1)从url指定的页面中检索内容;

2)从工作描述中提取所有词语;

3)创建一个word cloud。

建立这么简单的微服务花费不了多少时间,在下面会详细描述。在实际应用中,我们不可能在网上直接公开发布这些服务,因为没有身份验证、无法防止DOS攻击,没办法控制使用的用户。此外,我还准备提供一个带用户界面的app。所以我添加了一个MVC服务器,它将创建一个表示层。在微服务架构里,这实现也类似于API网关的模式。

由于微服务不需要大量的web应用程序组件,比如Session或用户管理等,使用Flask或Tornado建立Web应用似乎都是不错的选择。以为最近总是听到Tornado,我对它很好奇,所以选择使用它。关于如何使用Tornado创建Web应用程序,网上有很多例子,其中也包括一些谈论微服务的例子。基于这些示例,再加上最常用的语言是JSON,我编写了以下代码:

i            mport json            
            import tornado.ioloop
            import tornado.web
            from bs4 import BeautifulSoup
            
            class WordsHandler(tornado.web.RequestHandler):
                def get(self):
                    f = open("dice_job_page.html", "r")
                    html = f.read()
                    soup = BeautifulSoup(html, "html5lib")
                    job_desc = soup.find("div", id="jobdescSec")
                    job_text = job_desc.stripped_strings
                    words = ' '.join(job_text)
                    json_response = json.dumps({'data':words})
                    self.write(json_response)
            
            
            def make_app():
                return tornado.web.Application([
                    (r"/api/v1/words", WordsHandler),
                ])
            
            if __name__ == "__main__":
                app = make_app()
                app.listen(8888)
                tornado.ioloop.IOLoop.current().start()

这是最简单的代码,当执行此文件时,响应端口8888上的HTTP GET请求,该服务读取一个本地文件,使用html5lib和BeautifulSoup解析它,并返回JSON包装中的单词。

可以使用curl从命令行测试服务:

 $curl http://localhost:8888/api/v1/words

就是这样,我建立了一个微服务。我很兴奋。我几乎完成了!好的,也许它不应该每次从本地文件返回相同的响应。这似乎很容易解决,让我们继续。。

我觉得我需要多增加一些处理逻辑,服务不仅需要接受和响应输入内容,而且作为HTTP服务,它还应该返回至少一个状态代码。而且,每次通过发出请求来测试核心逻辑(提取文本),这看起来很麻烦。最后,虽然这并没有很多代码,但是将函数代码与框架隔离似乎是一个好主意,从而为其他服务设置约定,其中一些服务可能涉及更复杂的逻辑。

最后,我写了另一个文件,看起来是这样的:

           def get_words(html):            
              try:
                    soup = BeautifulSoup(html, "html5lib")# (1)
                    job_desc = soup.find("div", id="jobdescSec")# (2)
                    if not job_desc:
                        return None
                    else:
                        job_text = job_desc.stripped_strings# (3)
                        words = ' '.join(job_text)# (4)
                        json_response = json.dumps({'data':words})# (5)
                        return json_response
                except Exception as e:
                    return None
            
            class WordsHandler(tornado.web.RequestHandler):
                def get(self):
                    self.write("Breaking with all conventions, this API does not support GET")
            
                def post(self):
                    html = self.get_argument("html")
                    json_response = get_words(html)
                    if not json_response:
                        self.set_status(HTTP_STATUS_NO_CONTENT, 'There was no content')
                    else:
                        self.write(json_response)
                        self.set_header('Content-Type', 'application/json')
                        self.set_status(HTTP_STATUS_OK)

前面一到五行代码与原始版本完全相同。它们被隔离在一个名为get_words的函数中,该函数可以在不运行Tornado的情况下独立地进行单元测试。在处理程序本身代码中,有一些代码用于返回状态代码并设置其他HTTP头。如果有必要,还可以增加更多。

而设置和启动Tornado的代码则保留在原始文件中。

另外两个用于抓取页面内容和生成word Cloud的服务的代码结构也是大体相同的。

这里展示仅仅是URL抓取的代码。

            def get_data(url):            
                if not url:
                    return None
                try:
                    response = requests.get(url)
                    response64 = base64.encodebytes(response.content)
                    return response64.decode()
                except Exception as e:
                    return None
            
            class URLHandler(tornado.web.RequestHandler):
                def get(self):
                    url = self.get_argument("url")
                    data = get_data(url)
                    if not data:
                        self.set_status(HTTP_STATUS_NO_CONTENT, 'There was no content')
                    else:
                        self.write({'data': data})
                        self.set_header('Content-Type', 'application/json')
                        self.set_status(HTTP_STATUS_OK)

如果你想知道,为什么response.content是Base64编码的,因为它是一个字节数组,不是直接的JSON可序列化的。

这里是Make Wordcloud 代码。它使用word_cloud项目。

        def get_image(words):        
            if not words:
               return None
            try:
                # Generate a word cloud image using the word_cloud library
                wordcloud = WordCloud(max_font_size=80, width=960, height=540).generate(words)
                plt.imshow(wordcloud, interpolation='bilinear')
                plt.axis("off")
                pf = io.BytesIO()
                plt.savefig(pf, format='jpg')
                jpeg64 = base64.b64encode(pf.getvalue())
                return jpeg64.decode()
            except Exception as ex:
                return None
        
        class WordCloudHandler(tornado.web.RequestHandler):
            def get(self):
                self.write("Breaking with all conventions, this API does not support GET")
        
            def post(self):
                words = self.get_argument("words")
                image = get_image(words)
                if not image:
                    self.set_status(HTTP_STATUS_NO_CONTENT,'There was no content')
                else:
                    self.write(json.dumps({'data':image}))
                    self.set_header('Content-Type', 'application/json')
                    self.set_status(HTTP_STATUS_OK)

微服务建好之后,我只需要创建视图控制器来接收用户提交的url,使用这些微服务构建响应,并向用户发送响应。

我使用Django来构建应用服务器,因为我只想关注我需要的功能,而其他的内容可以由web应用程序来管理。

代码是这样的:

            class WordCloudView(TemplateView):            
                template_name = "cloudfun/wordcloud.html"
                form_class = WordCloudForm
            
                def get(self, request, *args, **kwargs):
                    form = self.form_class()
                    return render(request, self.template_name, {'form': form})
            
                def post(self, request, *args, **kwargs):
                    form = self.form_class(request.POST)
                    results = None
                    if form.is_valid():
                        url = form.cleaned_data['urls'].strip()
                        resp = requests.post('http://localhost:8888/api/v1/fromurl',data={'url':url})
                        html = resp.json()['data']
                        resp = requests.post('http://localhost:8887/api/v1/words',data={'html':html})
                        words = resp.json()['data']
                        resp = requests.post('http://localhost:8886/api/v1/wordcloud',data={'words':words})
                        results= resp.json()['data']
            
                    return render(request, self.template_name, {'form': form, 'results': results})

这个视图类的初始版本假设用户只输入了一个URL。这些服务都被hardcode到控制器中(稍后详细介绍)。一个微服务的响应直接插入到下一个微服务中。Django类非常简单,它只有两行:

            class WordCloudForm(forms.Form):            
                urls = forms.CharField(.Textarea)

the wordcloud.html 也没模板也很简单

            {% block content %}            
                Paste URLs into the following:
                <form action="{% url 'wordcloud' %}" method="post">
                    {% csrf_token %}
                    {{ form}}
                <input type="submit" value="OK">
                </form>
                {% if results %}
                    <div><img alt="Embedded Image" 
                              src="data:image/png;base64,{{ results }}" /></div>
                {% endif %}
            {% endblock %}

我现在已经编写了足够的代码来获取用户提交的URL,并向它们显示一个word cloud。是时候检验一下了。

我启动了三个微服务作为后台python任务:

            $ python microservices/cloud_creator/api_server.py &
            $ python microservices/fetch_url/api_server.py &
            $ python microservices/dice_scraper /api_server.py &

我在浏览器中启动了Django服务器和页面http://localhost:8000/cloudfun,使用从Dice.com网站获取的URL,然后单击OK。

它工作!

我在浏览器中看到了下面的图片。

从这个简单的微服务示例中,我被微服务的魅力吸引住了。它让我们思考,怎么样将一个大的系统分解成离散的服务,这也就是所谓的关注点分离。我们可以想象,如果您正在构建一个电子商务页面,需要获取商品搜索结果,您可能会启动十几个异步子请求,这些子请求都返回可以组装成一个页面的各种信息数据。在我的脑海里,我想象着一辆F1赛车停在一个维修站,一群工人猛扑上去,然后迅速把它恢复到正常状态,继续前行。

我花费了一个下午的时间完成上面的示例,还有一些代码需要改进。最大的问题是服务的位置被硬编码到视图控制器中。

当然,关注点分离长期以来一直是软件工程关注的焦点。面向对象编程也建议这么做。然后是CORBA,一个由10个IBM工程师组成的团队花了6个月的时间来功能。接下来是web Service和SOAP。当我在2001年为法国电信工作时,我对SOAP进行了评估,可以保证了互操作性。于是我使用Java Web Service来与.Net服务通信。结果发现各式各样的问题,我记得那简直地狱。人们一直在幻想Web服务的扩散,通过使用WSDL编写的服务契约自动被发现。会有航班预订网络服务,金融服务,如果有一个服务瘫痪了,系统就可以查到另一个,令人兴奋的东西。

快进15年,我们来到了微服务领域。但是,从我所看到的情况来看,微服务现在被限制为组织内客户提供服务,而不是对于开放互联网上的任何客户。以后或许微服务会走入互联网。

原文发布于微信公众号 - 程序你好(codinghello)

原文发表时间:2018-06-08

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏菩提树下的杨过

Gradle 10分钟上手指南

java的源码构建工具,大致经历了 ant -> maven -> gradle 这个过程,每一次进步,都是在解决之前的工具所带来的问题,简单来说: 1. an...

213100
来自专栏黑白安全

iOS安全基础之钥匙串与哈希

本文最初是由Chris Lowe编写的,后来经过Ryan Ackermann(ios系统开发者)的修改,已经可以针对最新的Xcode 9.2,Swift 4,i...

12120
来自专栏芋道源码1024

【追光者系列】HikariCP 连接池配多大合适(第一弹)?

首先声明一下观点:How big should HikariCP be? Not how big but rather how small!连接池的大小不是设置...

23500
来自专栏张善友的专栏

zookeeper 分布式锁服务

分布式锁服务在大家的项目中或许用的不多,因为大家都把排他放在数据库那一层来挡。当大量的行锁、表锁、事务充斥着数据库的时候。一般web应用很多的瓶颈都在数据库上,...

23280
来自专栏架构师之路

秒杀系统架构优化思路

一、秒杀业务为什么难做 1)im系统,例如qq或者微博,每个人都读自己的数据(好友列表、群列表、个人信息); 2)微博系统,每个人读你关注的人的数据,一个人读多...

503100
来自专栏风口上的猪的文章

.NET面试题系列[16] - 多线程概念(1)

这篇文章主要是各个百科中的一些摘抄,简述了进程和线程的来源,为什么出现了进程和线程。

24320
来自专栏草根专栏

使用 Moq 测试.NET Core - Why Moq?

在一个项目里, 我们经常需要把某一部分程序独立出来以便我们可以对这部分进行测试. 这就要求我们不要考虑项目其余部分的复杂性, 我们只想关注需要被测试的那部分. ...

16830
来自专栏信安之路

【读者投稿】wifi渗透-狸猫换太子

上期作者发布了一篇关于wifi钓鱼的方法,今天我来给大佬们带来一篇关于拿到wifi密码能干什么?

18000
来自专栏13blog.site

Java开源博客My-Blog之mysql容器重复初始化的严重bug修复过程

写在前面的话 My Blog项目已经开源了两个多月,也收获了不少star,在这里谢谢各位朋友的建议及帮助。由于个人原因,这个开源项目最初的定位其实是一个dock...

36870
来自专栏影子

给Ionic写一个cordova(PhoneGap)插件

501100

扫码关注云+社区

领取腾讯云代金券