无服务器的微服务 已翻译 100%

oschina 投递于 2015/09/07 17:22 (共 16 段, 翻译完成于 10-29)
阅读 6400
收藏 82
3
加载中

在 2015年的LinuxCon/ContainerCon 上我呈现了一次演示驱动的演讲,标题叫做“没有服务器的微型服务”。 其中,我创建了一个图片处理的微型服务,将其部署到了多个区域,构建了一个移动 app 并使用它(译者注:指的是这个微型服务)作为后台,添加了一个使用了 Amazon API 网关的基于 HTTP 的 API 和一个网站,并且对它进行了单元和负载测试,所有这些都没有用到任何服务器。

这篇博文对演讲的细节进行了重制,为你逐步完成所有必要的比周,并深入到了架构中去。而高层次的概述,可以看看这里的 幻灯片。还有这一架构的另外一个示例,可以看看这个可执行的 gist 资源库,SquirrelBin

LeoXu
LeoXu
翻译于 2015/09/11 21:34
2

无服务器架构

这里“无服务的”, 我们的意思是不需要明确的基础设施,如:没有服务器,没有要对服务器进行的部署,没有任何类型软件的安装。我们将只使用被管理的云服务和一台笔记本电脑。下面的图形描述了高级别的组件及他们的连接: 一个 Lambda 函数作为计算器(“后台”) 以及一个直接连接到计算器上的移动app, 再加上 Amazon API 网关,来提供一个 Amazon S3 所托管静态网站的 HTTP 端点.

        

一个使用 AWS Lambda 的移动和 Web 应用无服务器架构

现在,让我们开始构建吧!

步骤一 1: 创建图像处理服务

为了使得过程跟进起来更加容易一点,我们将使用一个内置了 Lambda 的 nodejs 语言库:ImageMagick。不过,那不是必须的 —— 如果你选择使用自己的库做替换,你可以 加载JavaScript或者本地库运行Python,或者甚至去封装一个 命令行的可执行程序。下面的示例使用 nodejs 实现的,但你也可以使用 JavaClojureScala 来构建这项服务, 或者使用 AWS Lambda 中其他基于 jvm 的语言。

LeoXu
LeoXu
翻译于 2015/09/12 10:21
1

下面的代码是一种“hello world” 类型的程序,用来演示 ImageMagick —— 它给我提供了一个基础的命令架构 (又叫做 switch 语句) 并且让我们可以获取到内置的玫瑰图片并返回它。除了对结果进行编码,那样它就可以很好的以 JSON 的形式存在,做这个并没有太多东西。

var im = require("imagemagick");
var fs = require("fs");
exports.handler = function(event, context) {
    if (event.operation) console.log("Operation " + event.operation + " requested");
    switch (event.operation) {
        case 'ping': context.succeed('pong'); return;
        case 'getSample':
            event.customArgs = ["rose:", "/tmp/rose.png"];
            im.convert(event.customArgs, function(err, output) {
                if (err) context.fail(err);
                else {
                    var resultImgBase64 = new Buffer(fs.readFileSync("/tmp/rose.png")).toString('base64');
                    try {fs.unlinkSync("/tmp/rose.png");} catch (e) {} // discard
                    context.succeed(resultImgBase64);
                }
            });
            break; // allow callback to complete
        default:
            var error = new Error('Unrecognized operation "' + event.operation + '"');
            context.fail(error);
            return;
    }
};

首先,让我们确保服务是运行着的,可以通过在 AWS Lambda 控制台的测试窗口向它发送下面的 JSON:

{
  "operation": "ping"
  }

你应该会得到必要的 “pong” 回应。接下来,我们将通过发送像下面这样的 JSON 来实际调用到  ImageMagick :

{
  "operation": "getSample"
  }

这一请求获取的是表示一张 PNG 版本玫瑰图片的 base64 编码的字符串: “”iVBORw0KGg…Jggg==”. 为了确认这个并不只是一些随机的字符, 将它复制粘贴(没有双引号) 到任何方便使用的 Base64-到-图片 解码器, 比如 codebeautify.org/base64-to-image-converter. 你应该能看到一张漂亮的玫瑰图片:

Sample Rose Image       

样例图片 (红玫瑰)

LeoXu
LeoXu
翻译于 2015/09/12 19:44
1

现在,让我们通过打开它的 Nodejs 包的剩余部分来完成图像处理服务。我们将提供一些不同的操作:

  • ping: 用于验证服务的可用性。

  • getDimensions: 用于调用识别(identify)操作来获取图像的宽度和高度的快捷方式。

  • identify: 获取图像元数据。

  • resize: 一个便捷的调整大小的程序(又称为封面图片的转换convert)。

  • thumbnail: resize的同义词。

  • convert: 一个万能程序 —— 可以转换媒体格式,应用变换,调整大小,等等。

  • getSample: 获取示例图像; 入门的基本操作。

Iam魔方
Iam魔方
翻译于 2015/09/14 12:11
2

大部分的代码是非常简单的由 Nodejs ImageMagick 封装的程序,其中一些以 JSON 方式编码(在这种情况下,传递给 Lambda 的事件被清理并向前传递),另一些以命令行(又名“自定义”)参数方式传递一个字符串数组。如果你之前从来没有使用过 ImageMagick,那么,ImageMagick 作为命令行的包装器并且文件名具有语义含义的要求可能是不被引起注意的。

我们有两个相互矛盾的需求:我们希望客户端传递语法格式(例如,输出图像的格式是 PNG 或者是 JPEG),但我们同时也要求服务器来决定在磁盘上何处放置临时存储,以便我们不遗漏具体的实现细节。为了同时实现这两个目标,我们在 JSON 模式中定义了两个参数:“inputExtension” 和“outputExtension”,然后,我们通过将客户端的部分(文件扩展名)与服务器的部分(目录和基名)相结合,构建实际的文件位置。你可以看到(和使用!)图像处理计划大纲(image processing blueprint)的已完成代码。

Iam魔方
Iam魔方
翻译于 2015/09/14 17:38
2

有很多测试你都可以在这里运行(我们也会在后面做更多工作),但就像一个快速而明智的检测一样,检索一个样本会再次创建图像并使用一个否定(颜色转变)过滤器将其回传。你可以在 Lambda 窗体中使用 JSON 这类工具,仅仅是用实际的图像单元替代基于 64 位的图像场(要在这个博客页面下包含这个有点长)。

{
  "operation": "convert",
  "customArgs": [
    "-negate"
  ],
  "outputExtension": "png",
  "base64Image": "...fill this in with the rose sample image, base64-encoded..."}

输出,解码为一个图像,应该是一个难懂的植物珍品,一个蓝玫瑰:

蓝色玫瑰(红色玫瑰样品图像的底片)

因此这所有的是服务的函数方面的内容。通常,在这个地方起初会变得丑陋,从“一次工作”到“具备 24x7x365 监控和生产记录的可伸缩和可靠的服务“。但这就是 Lambda 的漂亮所在:我们的图像进程代码已经是被完全摧毁了的,生产强度也是微服务。接下来,让我们加入一个可以寻呼的移动 app 吧...

木兰宿莽
木兰宿莽
翻译于 2015/09/15 23:27
2

步骤2: 创建一个移动客户端

我们的图像处理微服务可以以多种方式访问,但是为了展示一个样板客户端,我们将建立一个快速的Android app。下面我展示的客户端代码,是我们在 ContainerCon 演讲中创建的一个简单的 Android 应用程序。它允许你选择一个图像和一个滤波器,然后通过调用运行在 AWS Lambda 的图像处理服务的“转换”操作,最终显示使用过滤器处理后的图像效果。

下面的场景显示了应用程序的工作原理,其中一个是它的示例图片 --AWS Lambda 的图标:

Android 模拟器显示 AWS Lambda 的图标

我们将选择“相反(negate)”过滤器来反转图标的颜色:

Negate Selection

选择“相反(negate)”图像转换滤波器

下面是结果:一个蓝色版本的 Lambda 图标(原始版本为橙色):

Negated Icon Result 

使用“相反(negate)”滤镜处理后的 AWS Lambda 图标的结果

我们还可以选择西雅图照片并使用深褐色滤镜处理,使得图片中的现代的西雅图天空有一种怀旧感

Sepia-toned Seattle Skyline 

深褐色滤镜处理后的西雅图天空。

Iam魔方
Iam魔方
翻译于 2015/09/16 21:37
1

现在回到代码上面来吧。这里我不会试着去教授基础的 Android 编程,只特地专注于这个应用的 Lambda 元素。(如果你在创建自己的应用,你也会需要包含 AWS Mobile SDK 的 jar 包,以运行下面的示例代码) 。从概念上来讲有这么四个部分:

  1.  POJO 数据模式

  2.  远程服务(操作)定义

  3.  初始化

  4.  服务调用

我们将会逐一地来看看各个部分。

数模模式定义了任何需要在客户端和服务器之间进行传递的对象。这里没有“Lambda形式”的东西; 这些对象都只是 POJO(普通的 Java 对象),没有特殊的库或者框架。我们定义了一个基础事件,然后对它进行了扩展以反映我们的操作结构 – 你可以把这当做是之前我们定义和测试图像处理服务所用到的 JSON 的“Java 状态”。如果你也在使用 Java 编写服务端,那你通常就应该会把这些文件共享出来作为通用时间结构定义的一部分;在我们的示例中,这些 POJO 会在服务端被转换成 JSON。

LeoXu
LeoXu
翻译于 2015/09/17 20:25
1

LambdaEvent.java

package com.amazon.lambda.androidimageprocessor.lambda;
public class LambdaEvent {
    private String operation;
    public String getOperation() {return operation;}
    public void setOperation(String operation) {this.operation = operation;}
    public LambdaEvent(String operation) {setOperation(operation);}}

ImageConvertRequest.java

package com.amazon.lambda.androidimageprocessor.lambda;
import java.util.List;

public class ImageConvertRequest extends LambdaEvent {
    private String base64Image;
    private String inputExtension;
    private String outputExtension;
    private List customArgs;
    public ImageConvertRequest() {super("convert");}
    public String getBase64Image() {return base64Image;}
    public void setBase64Image(String base64Image) {this.base64Image = base64Image;}
    public String getInputExtension() {return inputExtension;}
    public void setInputExtension(String inputExtension) {this.inputExtension = inputExtension;}
    public String getOutputExtension() {return outputExtension;}
    public void setOutputExtension(String outputExtension) {this.outputExtension = outputExtension;}
    public List getCustomArgs() {return customArgs;}
    public void setCustomArgs(List customArgs) {this.customArgs = customArgs;}}

到目前为止还不是很复杂。现在我们有了一个数据模型,再就是将要使用一些 Java 注解来定义服务端点。这里我们会暴露出两个操作, “ping” 以及“convert”; 这也能很容易通过添加其它注解来对其进行扩展,但就下面这个示例应用而言,我们暂时还不需要这么做。

ILambdaInvoker.java

package com.amazon.lambda.androidimageprocessor.lambda;
import com.amazonaws.mobileconnectors.lambdainvoker.LambdaFunction;
import java.util.Map;
public interface ILambdaInvoker {
    @LambdaFunction(functionName = "ImageProcessor")
    String ping(Map event);
    @LambdaFunction(functionName = "ImageProcessor")
    String convert(ImageConvertRequest request);}

现在我们已经准备好来做这个应用主要部分了。这里大部分都是样板式的 Android 代码或者简单客户端资源管理,而我将会点出几个跟 Lambda 相关的部分:

这就是“init”部分;它创建了身份验证功能来调用 Lambda API 并创建了一个能够调用上面所定义的端点,而且能在我们的数据模型中传送 POJO 的 Lambda 调用:

 // Create an instance of CognitoCachingCredentialsProvider
        CognitoCachingCredentialsProvider cognitoProvider = new CognitoCachingCredentialsProvider(
                this.getApplicationContext(), "us-east-1:<YOUR COGNITO IDENITY POOL GOES HERE>", Regions.US_EAST_1);
        // Create LambdaInvokerFactory, to be used to instantiate the Lambda proxy.
        LambdaInvokerFactory factory = new LambdaInvokerFactory(this.getApplicationContext(),
                Regions.US_EAST_1, cognitoProvider);
        // Create the Lambda proxy object with a default Json data binder.
        lambda = factory.build(ILambdaInvoker.class);

其余的也挺有趣的部分代码就是它自身实际的远程过程调用了:

                try {
                    return lambda.convert(params[0]);
                } catch (LambdaFunctionException e) {
                    Log.e("Tag", "Failed to convert image");
                    return null;
                }

实际上也不那么有趣,因为这戏法(参数序列化和结果的反序列化)是发生在幕后的,留给我们的仅仅只是一些错误的处理而已。

下面是完整的代码文件:

MainActivity.java

package com.amazon.lambda.androidimageprocessor;

import android.app.Activity;
import android.app.ProgressDialog;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Base64;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;
import android.widget.Spinner;
import android.widget.Toast;
import com.amazon.lambda.androidimageprocessor.lambda.ILambdaInvoker;
import com.amazon.lambda.androidimageprocessor.lambda.ImageConvertRequest;
import com.amazonaws.auth.CognitoCachingCredentialsProvider;
import com.amazonaws.mobileconnectors.lambdainvoker.LambdaFunctionException;
import com.amazonaws.mobileconnectors.lambdainvoker.LambdaInvokerFactory;
import com.amazonaws.regions.Regions;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

public class MainActivity extends Activity {
    private ILambdaInvoker lambda;
    private ImageView selectedImage;
    private String selectedImageBase64;
    private ProgressDialog progressDialog;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // Create an instance of CognitoCachingCredentialsProvider
        CognitoCachingCredentialsProvider cognitoProvider = new CognitoCachingCredentialsProvider(
                this.getApplicationContext(), "us-east-1:2a40105a-b330-43cf-8d4e-b647d492e76e", Regions.US_EAST_1);
        // Create LambdaInvokerFactory, to be used to instantiate the Lambda proxy.
        LambdaInvokerFactory factory = new LambdaInvokerFactory(this.getApplicationContext(),
                Regions.US_EAST_1, cognitoProvider);
        // Create the Lambda proxy object with a default Json data binder.
        lambda = factory.build(ILambdaInvoker.class);
        // ping lambda function to make sure everything is working
        pingLambda();
    }
    // ping the lambda function
    @SuppressWarnings("unchecked")
    private void pingLambda() {
        Map event = new HashMap();
        event.put("operation", "ping");
        // The Lambda function invocation results in a network call.
        // Make sure it is not called from the main thread.
        new AsyncTask<Map, Void, String>() {
            @Override
            protected String doInBackground(Map... params) {
                // invoke "ping" method. In case it fails, it will throw a
                // LambdaFunctionException.
                try {
                    return lambda.ping(params[0]);
                } catch (LambdaFunctionException lfe) {
                    Log.e("Tag", "Failed to invoke ping", lfe);
                    return null;
                }
            }
            @Override
            protected void onPostExecute(String result) {
                if (result == null) {
                    return;
                }
                // Display a quick message
                Toast.makeText(MainActivity.this, "Made contact with AWS lambda", Toast.LENGTH_LONG).show();
            }
        }.execute(event);
    }
    // event handler for "process image" button
    public void processImage(View view) {
        // no image has been selected yet
        if (selectedImageBase64 == null) {
            Toast.makeText(this, "Please tap one of the images above", Toast.LENGTH_LONG).show();
            return;
        }
        // get selected filter
        String filter = ((Spinner) findViewById(R.id.filter_picker)).getSelectedItem().toString();
        // assemble new request
        ImageConvertRequest request = new ImageConvertRequest();
        request.setBase64Image(selectedImageBase64);
        request.setInputExtension("png");
        request.setOutputExtension("png");
        // custom arguments per filter
        List customArgs = new ArrayList();
        request.setCustomArgs(customArgs);
        switch (filter) {
            case "Sepia":
                customArgs.add("-sepia-tone");
                customArgs.add("65%");
                break;
            case "Black/White":
                customArgs.add("-colorspace");
                customArgs.add("Gray");
                break;
            case "Negate":
                customArgs.add("-negate");
                break;
            case "Darken":
                customArgs.add("-fill");
                customArgs.add("black");
                customArgs.add("-colorize");
                customArgs.add("50%");
                break;
            case "Lighten":
                customArgs.add("-fill");
                customArgs.add("white");
                customArgs.add("-colorize");
                customArgs.add("50%");
                break;
            default:
                return;
        }
        // async request to lambda function
        new AsyncTask() {
            @Override
            protected String doInBackground(ImageConvertRequest... params) {
                try {
                    return lambda.convert(params[0]);
                } catch (LambdaFunctionException e) {
                    Log.e("Tag", "Failed to convert image");
                    return null;
                }
            }
            @Override
            protected void onPostExecute(String result) {
                // if no data was returned, there was a failure
                if (result == null || Objects.equals(result, "")) {
                    hideLoadingDialog();
                    Toast.makeText(MainActivity.this, "Processing failed", Toast.LENGTH_LONG).show();
                    return;
                }
                // otherwise decode the base64 data and put it in the selected image view
                byte[] imageData = Base64.decode(result, Base64.DEFAULT);
                selectedImage.setImageBitmap(BitmapFactory.decodeByteArray(imageData, 0, imageData.length));
                hideLoadingDialog();
            }
        }.execute(request);
        showLoadingDialog();
    }
    /*
    Select methods for each image
     */
    public void selectLambdaImage(View view) {
        selectImage(R.drawable.lambda);
        selectedImage = (ImageView) findViewById(R.id.static_lambda);
        Toast.makeText(this, "Selected image 'lambda'", Toast.LENGTH_LONG).show();
    }
    public void selectSeattleImage(View view) {
        selectImage(R.drawable.seattle);
        selectedImage = (ImageView) findViewById(R.id.static_seattle);
        Toast.makeText(this, "Selected image 'seattle'", Toast.LENGTH_LONG).show();
    }
    public void selectSquirrelImage(View view) {
        selectImage(R.drawable.squirrel);
        selectedImage = (ImageView) findViewById(R.id.static_squirrel);
        Toast.makeText(this, "Selected image 'squirrel'", Toast.LENGTH_LONG).show();
    }
    public void selectLinuxImage(View view) {
        selectImage(R.drawable.linux);
        selectedImage = (ImageView) findViewById(R.id.static_linux);
        Toast.makeText(this, "Selected image 'linux'", Toast.LENGTH_LONG).show();
    }
    // extract the base64 encoded data of the drawable resource `id`
    private void selectImage(int id) {
        Bitmap bmp = BitmapFactory.decodeResource(getResources(), id);
        ByteArrayOutputStream stream = new ByteArrayOutputStream();
        bmp.compress(Bitmap.CompressFormat.PNG, 100, stream);
        selectedImageBase64 = Base64.encodeToString(stream.toByteArray(), Base64.DEFAULT);
    }
    // reset images to their original state
    public void reset(View view) {
        ((ImageView) findViewById(R.id.static_lambda)).setImageDrawable(getResources().getDrawable(R.drawable.lambda, getTheme()));
        ((ImageView) findViewById(R.id.static_seattle)).setImageDrawable(getResources().getDrawable(R.drawable.seattle, getTheme()));
        ((ImageView) findViewById(R.id.static_squirrel)).setImageDrawable(getResources().getDrawable(R.drawable.squirrel, getTheme()));
        ((ImageView) findViewById(R.id.static_linux)).setImageDrawable(getResources().getDrawable(R.drawable.linux, getTheme()));
        Toast.makeText(this, "Please choose from one of these images", Toast.LENGTH_LONG).show();
    }
    private void showLoadingDialog() {
        progressDialog = ProgressDialog.show(this, "Please wait...", "Processing image", true, false);
    }
    private void hideLoadingDialog() {
        progressDialog.dismiss();
    }
}

这就是这个移动应用所需要的了:一个数据模型(又叫做 Java 类),一个控制模型(又叫做成对的方法),三个用来对一些东西进行初始化的语句,而后就是一个被 try/catch 块包围起来的远程调用了 … 够简单。

LeoXu
LeoXu
翻译于 2015/09/17 20:41
2

多区域部署

到目前为止我们还没有更多讨论代码运行的环境。Lambda 会指定一个区域部署你的代码,但你必须决定你想要在哪个(或哪些)区域运行它。在我初始的版本中,我在美国东1区(又名弗吉尼亚数据中心)创建了初始程序。为了能够在网络中获得更好地体验,我们建立了一个全球性的服务,我们把它扩展到包括 eu-west-1(爱尔兰)和 ap-northeast-1(东京),这样我们的移动应用程序可以从世界各地快速地连接:

Cross-region Auto-deployment from Amazon S3 with an AWS Lambda function一种在两个附加的区域内部署 Lambda 功能的无服务器机制

下面的内容我们已经在博客中提到:在 S3 部署博客中,我展示了如何使用 lambda 函数部署其他存储在亚马逊 S3 的 lambda 函数压缩文件。在 ContainerCon 演示中,我们搭建了小型的平台并打开了 S3 跨区域复制,这样我们就可以以 ZIP 压缩文件的方式上传图片处理服务到爱尔兰数据中心,并自动拷贝到东京数据中心,然后将部署在两个区域的服务关连起来,形成了各自区域的 Lambda 服务。

快来享受无服务器的解决方案吧:)

Iam魔方
Iam魔方
翻译于 2015/09/18 23:15
2
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接。
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
加载中

评论(9)

vlaw
vlaw
云主机和lambda服务不是回事儿,不是在虚拟机上部署tomcat,评论有点辜负译者了
muwanqing123
muwanqing123
你好
jQer
jQer
我忽然想起了那天有个哥们跟我说,他们要迁移到云服务器。跟我说他们的小组在讨论,一迁移很多接口都要重新修改,好多服务都不能用。我就纳了闷了,你从本地服务器上云服务器,部署下装上就完事了,哪来的修改。扯拉半天,原来数据库的接口全是用的其他服务商提供的。我心说,这不自找的。哥的服务一向都是手写,所以我们的没有修改问题。
jQer
jQer
100% 其他服务商提供的 HTTP 包协议,这辈子就是个混的命了。
slver888
slver888
这不就类似僵尸蠕虫病毒吗,感染每个电脑然后劫持系统提供某种网络服务。
素妆
素妆
79
zhenruyan
zhenruyan
所以还是有服务器…只不过简化和服务器的联系方式了
技安
技安
如果某个api由于政策的改变发生了变化甚至取消,这种“无服务的”架构对于应用来说是一种毁灭性的打击。
木川瓦兹
木川瓦兹
云也是服务器吧,只不过是服务器的集合而已
返回顶部
顶部