38、构建容错的图像应用程序

构建容错的图像应用程序

1. 幂等状态转换

在构建应用程序时,状态转换的幂等性非常重要。幂等状态转换意味着无论转换发生多少次,结果都是相同的。例如,在实现 Created -> Uploaded 状态转换时,最初的实现可能不是幂等的。

最初的实现代码如下:

uploaded(s3Key) {
    process = DynamoDB.getItem(processId)
    if (process.state !== 'Created') {
        throw new Error('transition not allowed')
    }
    DynamoDB.updateItem(processId, {'state': 'Uploaded', 'rawS3Key': s3Key})
    SQS.sendMessage({'processId': processId, 'action': 'process'});
}

这个实现存在问题,如果 SQS.sendMessage 失败,状态转换会失败,重试时会抛出 “transition not allowed” 错误。为了使其幂等,需要修改 if 语句:

uploaded(s3Key) {
    process = DynamoDB.getItem(processId)
    if (process.state !== 'Created' && process.state !== 'Uploaded') {
        throw new Error('transition not allowed')
    }
    DynamoDB.updateItem(processId, {'state': 'Uploaded', 'rawS3Key': s3Key})
    SQS.sendMessage({'processId': processId, 'action': 'process'});
}

这样,即使重试,多次更新 DynamoDB 和发送多个 SQS 消息也不会有问题,因为 SQS 消息消费者也必须是幂等的。同样的原则也适用于 Uploaded -> Processed 转换。

2. 实现容错的 Web 服务

将图像应用程序分为两部分:Web 服务器和工作器。Web 服务器为用户提供 REST API,工作器处理图像。

REST API 支持以下路由:
- POST /image :创建一个新的图像处理流程。
- GET /image/:id :返回指定 ID 的处理流程的状态。
- POST /image/:id/upload :为指定 ID 的处理流程上传文件。

2.1 设置 Web 服务器项目

使用 Node.js 和 Express 框架实现 Web 服务器,需要一些初始化代码:

var express = require('express');
var bodyParser = require('body-parser');
var AWS = require('aws-sdk');
var uuidv4 = require('uuid/v4');
var multiparty = require('multiparty');
var db = new AWS.DynamoDB({
    'region': 'us-east-1'
});
var sqs = new AWS.SQS({
    'region': 'us-east-1'
});
var s3 = new AWS.S3({
    'region': 'us-east-1'
});
var app = express();
app.use(bodyParser.json());
app.listen(process.env.PORT || 8080, function() {
    console.log('Server started. Open http://localhost:'
        + (process.env.PORT || 8080) + ' with browser.');
});
2.2 创建新的图像处理流程

添加一个路由来处理 POST /image 请求:

app.post('/image', function(request, response) {
    var id = uuidv4();
    db.putItem({
        'Item': {
            'id': {
                'S': id
            },
            'version': {
                'N': '0'
            },
            'created': {
                'N': Date.now().toString()
            },
            'state': {
                'S': 'created'
            }
        },
        'TableName': 'imagery-image',
        'ConditionExpression': 'attribute_not_exists(id)'
    }, function(err, data) {
        if (err) {
            throw err;
        } else {
            response.json({'id': id, 'state': 'created'});
        }
    });
});

这里使用了乐观锁机制,通过 version 属性确保数据一致性。

2.3 查找图像处理流程

添加一个路由来处理 GET /image/:id 请求:

function mapImage = function(item) {
    return {
        'id': item.id.S,
        'version': parseInt(item.version.N, 10),
        'state': item.state.S,
        'rawS3Key': // [...]
        'processedS3Key': // [...]
        'processedImage': // [...]
    };
};
function getImage(id, cb) {
    db.getItem({
        'Key': {
            'id': {
                'S': id
            }
        },
        'TableName': 'imagery-image'
    }, function(err, data) {
        if (err) {
            cb(err);
        } else {
            if (data.Item) {
                cb(null, lib.mapImage(data.Item));
            } else {
                cb(new Error('image not found'));
            }
        }
    });
}
app.get('/image/:id', function(request, response) {
    getImage(request.params.id, function(err, image) {
        if (err) {
            throw err;
        } else {
            response.json(image);
        }
    });
});
2.4 上传图像

上传图像需要几个步骤:
1. 将原始图像上传到 S3。
2. 修改 DynamoDB 中的项目。
3. 发送 SQS 消息以触发处理。

实现代码如下:

function uploadImage(image, part, response) {
    var rawS3Key = 'upload/' + image.id + '-' + Date.now();
    s3.putObject({
        'Bucket': process.env.ImageBucket,
        'Key': rawS3Key,
        'Body': part,
        'ContentLength': part.byteCount
    }, function(err, data) {
        if (err) {
            throw err;
        } else {
            db.updateItem({
                'Key': {
                    'id': {
                        'S': image.id
                    }
                },
                'UpdateExpression': 'SET #s=:newState, version=:newVersion, rawS3Key=:rawS3Key',
                'ConditionExpression': 'attribute_exists(id) AND version=:oldVersion AND #s IN (:stateCreated, :stateUploaded)',
                'ExpressionAttributeNames': {
                    '#s': 'state'
                },
                'ExpressionAttributeValues': {
                    ':newState': {
                        'S': 'uploaded'
                    },
                    ':oldVersion': {
                        'N': image.version.toString()
                    },
                    ':newVersion': {
                        'N': (image.version + 1).toString()
                    },
                    ':rawS3Key': {
                        'S': rawS3Key
                    },
                    ':stateCreated': {
                        'S': 'created'
                    },
                    ':stateUploaded': {
                        'S': 'uploaded'
                    }
                },
                'ReturnValues': 'ALL_NEW',
                'TableName': 'imagery-image'
            }, function(err, data) {
                if (err) {
                    throw err;
                } else {
                    sqs.sendMessage({
                        'MessageBody': JSON.stringify({
                            'imageId': image.id,
                            'desiredState': 'processed'
                        }),
                        'QueueUrl': process.env.ImageQueue
                    }, function(err) {
                        if (err) {
                            throw err;
                        } else {
                            response.redirect('/#view=' + image.id);
                            response.end();
                        }
                    });
                }
            });
        }
    });
}
app.post('/image/:id/upload', function(request, response) {
    getImage(request.params.id, function(err, image) {
        if (err) {
            throw err;
        } else {
            var form = new multiparty.Form();
            form.on('part', function(part) {
                uploadImage(image, part, response);
            });
            form.parse(request);
        }
    });
});
3. 实现容错的工作器以消费 SQS 消息

工作器负责处理图像,应用棕褐色滤镜。使用 Elastic Beanstalk 来消费 SQS 消息并执行 HTTP POST 请求。

3.1 设置服务器项目

同样使用 Node.js 和 Express 框架,初始化代码如下:

var express = require('express');
var bodyParser = require('body-parser');
var AWS = require('aws-sdk');
var assert = require('assert-plus');
var Caman = require('caman').Caman;
var fs = require('fs');
var db = new AWS.DynamoDB({
    'region': 'us-east-1'
});
var s3 = new AWS.S3({
    'region': 'us-east-1'
});
var app = express();
app.use(bodyParser.json());
app.get('/', function(request, response) {
    response.json({});
});
app.listen(process.env.PORT || 8080, function() {
    console.log('Worker started on port ' + (process.env.PORT || 8080));
});
3.2 处理 SQS 消息和处理图像

当接收到 SQS 消息时,工作器下载原始图像,应用棕褐色滤镜,上传处理后的图像,并修改 DynamoDB 中的处理状态。

function processImage(image, cb) {
    var processedS3Key = 'processed/' + image.id + '-' + Date.now() + '.png';
    // download raw image from S3
    // process image
    // upload sepia image to S3
    cb(null, processedS3Key);
}
function processed(image, request, response) {
    processImage(image, function(err, processedS3Key) {
        if (err) {
            throw err;
        } else {
            db.updateItem({
                'Key': {
                    'id': {
                        'S': image.id
                    }
                },
                'UpdateExpression': 'SET #s=:newState, version=:newVersion, processedS3Key=:processedS3Key',
                'ConditionExpression': 'attribute_exists(id) AND version=:oldVersion AND #s IN (:stateUploaded, :stateProcessed)',
                'ExpressionAttributeNames': {
                    '#s': 'state'
                },
                'ExpressionAttributeValues': {
                    ':newState': {
                        'S': 'processed'
                    },
                    ':oldVersion': {
                        'N': image.version.toString()
                    },
                    ':newVersion': {
                        'N': (image.version + 1).toString()
                    },
                    ':processedS3Key': {
                        'S': processedS3Key
                    },
                    ':stateUploaded': {
                        'S': 'uploaded'
                    },
                    ':stateProcessed': {
                        'S': 'processed'
                    }
                },
                'ReturnValues': 'ALL_NEW',
                'TableName': 'imagery-image'
            }, function(err, data) {
                if (err) {
                    throw err;
                } else {
                    response.json(lib.mapImage(data.Attributes));
                }
            });
        }
    });
}
app.post('/sqs', function(request, response) {
    assert.string(request.body.imageId, 'imageId');
    assert.string(request.body.desiredState, 'desiredState');
    getImage(request.body.imageId, function(err, image) {
        if (err) {
            throw err;
        } else {
            if (request.body.desiredState === 'processed') {
                processed(image, request, response);
            } else {
                throw new Error("unsupported desiredState");
            }
        }
    });
});
4. 部署应用程序

使用 Elastic Beanstalk 和 CloudFormation 来部署服务器和工作器。CloudFormation 定义了以下资源:
- S3 存储桶,用于存储原始和处理后的图像。
- DynamoDB 表 imagery-image
- SQS 队列和死信队列。
- 服务器和工作器 EC2 实例的 IAM 角色。
- 服务器和工作器的 Elastic Beanstalk 应用程序。

4.1 部署 S3、DynamoDB 和 SQS

CloudFormation 模板如下:

---
AWSTemplateFormatVersion: '2010-09-09'
Description: 'AWS in Action: chapter 16'
Parameters:
  KeyName:
    Description: 'Key Pair name'
    Type: 'AWS::EC2::KeyPair::KeyName'
    Default: mykey
Resources:
  Bucket:
    Type: 'AWS::S3::Bucket'
    Properties:
      BucketName: !Sub 'imagery-${AWS::AccountId}'
      WebsiteConfiguration:
        ErrorDocument: error.html
        IndexDocument: index.html
  Table:
    Type: 'AWS::DynamoDB::Table'
    Properties:
      AttributeDefinitions:
        - AttributeName: id
          AttributeType: S
      KeySchema:
        - AttributeName: id
          KeyType: HASH
      ProvisionedThroughput:
        ReadCapacityUnits: 1
        WriteCapacityUnits: 1
      TableName: 'imagery-image'
  SQSDLQueue:
    Type: 'AWS::SQS::Queue'
    Properties:
      QueueName: 'message-dlq'
  SQSQueue:
    Type: 'AWS::SQS::Queue'
    Properties:
      QueueName: message
      RedrivePolicy:
        deadLetterTargetArn: !Sub '${SQSDLQueue.Arn}'
        maxReceiveCount: 10
Outputs:
  EndpointURL:
    Value: !Sub 'http://${EBServerEnvironment.EndpointURL}'
    Description: Load Balancer URL

这里引入了死信队列的概念,如果一个 SQS 消息多次重试失败,会被转移到死信队列。

4.2 IAM 角色

服务器实例需要的权限:
- sqs:SendMessage 到 SQS 队列。
- s3:PutObject 到 S3 存储桶。
- dynamodb:GetItem dynamodb:PutItem dynamodb:UpdateItem 到 DynamoDB 表。
- cloudwatch:PutMetricData (Elastic Beanstalk 要求)。
- s3:Get* s3:List* s3:PutObject (Elastic Beanstalk 要求)。

工作器实例需要的权限:
- sqs:ChangeMessageVisibility sqs:DeleteMessage sqs:ReceiveMessage 到 SQS 队列。
- s3:PutObject 到 S3 存储桶。
- dynamodb:GetItem dynamodb:UpdateItem 到 DynamoDB 表。
- cloudwatch:PutMetricData (Elastic Beanstalk 要求)。
- s3:Get* s3:List* s3:PutObject (Elastic Beanstalk 要求)。

4.3 Elastic Beanstalk 部署服务器

Elastic Beanstalk 应用程序由版本、环境和配置组成。以下是部署服务器的 CloudFormation 模板部分:

EBServerApplication:
  Type: 'AWS::ElasticBeanstalk::Application'
  Properties:
    ApplicationName: 'imagery-server'
    Description: 'Imagery server: AWS in Action: chapter 16'
EBServerConfigurationTemplate:
  Type: 'AWS::ElasticBeanstalk::ConfigurationTemplate'
  Properties:
    ApplicationName: !Ref EBServerApplication
    Description: 'Imagery server: AWS in Action: chapter 16'
    SolutionStackName: '64bit Amazon Linux 2017.09 v4.4.0 running Node.js'
    OptionSettings:
      - Namespace: 'aws:autoscaling:asg'
        OptionName: 'MinSize'
        Value: '2'
      - Namespace: 'aws:autoscaling:launchconfiguration'
        OptionName: 'EC2KeyName'
        Value: !Ref KeyName
      - Namespace: 'aws:autoscaling:launchconfiguration'
        OptionName: 'IamInstanceProfile'
        Value: !Ref ServerInstanceProfile
      - Namespace: 'aws:elasticbeanstalk:container:nodejs'
        OptionName: 'NodeCommand'
        Value: 'node server.js'
      - Namespace: 'aws:elasticbeanstalk:application:environment'
        OptionName: 'ImageQueue'
        Value: !Ref SQSQueue
      - Namespace: 'aws:elasticbeanstalk:application:environment'
        OptionName: 'ImageBucket'
        Value: !Ref Bucket
      - Namespace: 'aws:elasticbeanstalk:container:nodejs:staticfiles'
        OptionName: '/public'
        Value: '/public'
EBServerApplicationVersion:
  Type: 'AWS::ElasticBeanstalk::ApplicationVersion'
  Properties:
    ApplicationName: !Ref EBServerApplication
    Description: 'Imagery server: AWS in Action: chapter 16'
    SourceBundle:
      S3Bucket: 'awsinaction-code2'
      S3Key: 'chapter16/build/server.zip'
EBServerEnvironment:
  Type: 'AWS::ElasticBeanstalk::Environment'
  Properties:
    ApplicationName: !Ref EBServerApplication
    Description: 'Imagery server: AWS in Action: chapter 16'
    TemplateName: !Ref EBServerConfigurationTemplate
    VersionLabel: !Ref EBServerApplicationVersion
4.4 Elastic Beanstalk 部署工作器

部署工作器的 CloudFormation 模板部分如下:

EBWorkerApplication:
  Type: 'AWS::ElasticBeanstalk::Application'
  Properties:
    ApplicationName: 'imagery-worker'
    Description: 'Imagery worker: AWS in Action: chapter 16'
EBWorkerConfigurationTemplate:
  Type: 'AWS::ElasticBeanstalk::ConfigurationTemplate'
  Properties:
    ApplicationName: !Ref EBWorkerApplication
    Description: 'Imagery worker: AWS in Action: chapter 16'
    SolutionStackName: '64bit Amazon Linux 2017.09 v4.4.0 running Node.js'
    OptionSettings:
      - Namespace: 'aws:autoscaling:launchconfiguration'
        OptionName: 'EC2KeyName'
        Value: !Ref KeyName
      - Namespace: 'aws:autoscaling:launchconfiguration'
        OptionName: 'IamInstanceProfile'
        Value: !Ref WorkerInstanceProfile
      - Namespace: 'aws:elasticbeanstalk:sqsd'
        OptionName: 'WorkerQueueURL'
        Value: !Ref SQSQueue
      - Namespace: 'aws:elasticbeanstalk:sqsd'
        OptionName: 'HttpPath'
        Value: '/sqs'
      - Namespace: 'aws:elasticbeanstalk:container:nodejs'
        OptionName: 'NodeCommand'
        Value: 'node worker.js'
      - Namespace: 'aws:elasticbeanstalk:application:environment'
        OptionName: 'ImageQueue'
        Value: !Ref SQSQueue
      - Namespace: 'aws:elasticbeanstalk:application:environment'
        OptionName: 'ImageBucket'
        Value: !Ref Bucket
EBWorkerApplicationVersion:
  Type: 'AWS::ElasticBeanstalk::ApplicationVersion'
  Properties:
    ApplicationName: !Ref EBWorkerApplication
    Description: 'Imagery worker: AWS in Action: chapter 16'
    SourceBundle:
      S3Bucket: 'awsinaction-code2'
      S3Key: 'chapter16/build/worker.zip'
EBWorkerEnvironment:
  Type: 'AWS::ElasticBeanstalk::Environment'
  Properties:
    ApplicationName: !Ref EBWorkerApplication
    Description: 'Imagery worker: AWS in Action: chapter 16'
    TemplateName: !Ref EBWorkerConfigurationTemplate
    VersionLabel: !Ref EBWorkerApplicationVersion
    Tier:
      Type: 'SQS/HTTP'
      Name: 'Worker'
      Version: '1.0'
5. 清理资源

部署完成后,如果需要清理资源,可以按以下步骤操作:
1. 获取 S3 存储桶名称:

aws cloudformation describe-stack-resource --stack-name imagery \
    --logical-resource-id Bucket \
    --query "StackResourceDetail.PhysicalResourceId" \
    --output text
  1. 删除 S3 存储桶中的所有文件:
aws s3 rm s3://$bucketname --recursive
  1. 删除 CloudFormation 堆栈:
aws cloudformation delete-stack --stack-name imagery

通过以上步骤,你可以构建一个容错的图像应用程序,并在 AWS 上进行部署和管理。

总结

  • 容错意味着预期会发生故障,并设计系统以应对故障。
  • 可以使用幂等操作来实现从一个状态到另一个状态的转换,以创建容错应用程序。
  • 为了实现容错,状态不应驻留在 EC2 实例上(无状态服务器)。
  • AWS 提供了容错服务和工具,可用于创建容错系统。EC2 本身不是开箱即用的容错服务,但可以使用多个 EC2 实例和自动扩展组来消除单点故障。

构建容错的图像应用程序

6. 容错机制的优势与原理

在构建图像应用程序的过程中,采用的容错机制具有显著优势。幂等状态转换是核心策略之一,它确保了系统在面对失败重试时的稳定性。例如,在状态转换 Created -> Uploaded 中,最初非幂等的实现可能会因部分操作失败导致后续重试报错。而通过修改条件判断,使其变为幂等操作后,即使多次重试,也不会影响系统的最终状态,避免了数据不一致的问题。

乐观锁机制在数据更新时起到了关键作用。通过在 DynamoDB 操作中引入 version 属性,确保了同一时间只有一个进程能够成功更新数据,防止了数据冲突。当多个进程尝试更新同一数据时,只有版本号匹配的更新请求会被接受,其他请求将被拒绝,从而保证了数据的一致性和完整性。

7. 系统架构分析

整个图像应用程序采用了分布式架构,分为 Web 服务器和工作器两部分。这种架构设计使得系统具有良好的可扩展性和容错性。

Web 服务器负责提供 REST API 给用户,处理用户的请求,如创建新的图像处理流程、查询处理状态和上传图像等。通过使用 Express 框架,能够快速搭建起稳定的 Web 服务。同时,使用多个 EC2 实例和负载均衡器(ELB),可以将用户请求均匀地分配到不同的实例上,避免了单点故障。

工作器则负责处理图像,应用棕褐色滤镜等操作。它通过消费 SQS 消息来触发图像的处理任务,实现了异步处理,提高了系统的响应速度和处理效率。Elastic Beanstalk 的使用使得工作器的部署和管理变得更加简单,它能够自动处理 SQS 消息的消费和应用的部署。

以下是系统架构的 mermaid 流程图:

graph LR
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
    User[/用户/]:::process -->|请求| ELB(负载均衡器):::process
    ELB -->|分发请求| WebServer(Web 服务器):::process
    WebServer -->|存储信息| DynamoDB(数据库):::process
    WebServer -->|上传图像| S3(存储桶):::process
    WebServer -->|触发任务| SQS(消息队列):::process
    SQS -->|消息| Worker(工作器):::process
    Worker -->|下载图像| S3
    Worker -->|处理图像| S3
    Worker -->|更新状态| DynamoDB
8. 部署与管理要点

使用 Elastic Beanstalk 和 CloudFormation 进行应用程序的部署,具有自动化和可重复性的优势。CloudFormation 模板定义了所有必要的资源,包括 S3 存储桶、DynamoDB 表、SQS 队列和 IAM 角色等,确保了部署的一致性和准确性。

在部署过程中,需要注意以下几点:
- 资源创建顺序 :确保各个资源按照正确的顺序创建,例如先创建 S3 存储桶和 DynamoDB 表,再部署 Elastic Beanstalk 应用程序。
- 权限配置 :为服务器和工作器实例分配正确的 IAM 角色和权限,确保它们能够正常访问所需的资源。
- 环境变量设置 :在 Elastic Beanstalk 配置中,正确设置环境变量,如 S3 存储桶名称、SQS 队列 URL 等,以便应用程序能够正常运行。

9. 故障处理与监控

为了确保系统的稳定性,需要建立完善的故障处理和监控机制。

  • 死信队列 :在 SQS 中使用死信队列,当消息多次处理失败时,将其转移到死信队列中。这样可以避免消息在队列中无限重试,浪费系统资源。同时,可以设置 CloudWatch 警报,当死信队列中有消息时及时通知管理员进行排查。
  • 日志监控 :使用 CloudWatch 监控系统的日志和指标,及时发现系统中的异常情况。例如,监控 EC2 实例的 CPU 使用率、内存使用率等指标,当指标超过阈值时及时采取措施。
  • 重试机制 :在应用程序中实现重试机制,当部分操作失败时,自动进行重试。但需要注意重试次数的限制,避免无限重试导致系统资源耗尽。
10. 性能优化建议

为了提高系统的性能,可以考虑以下优化建议:
- 缓存机制 :在 Web 服务器和工作器中引入缓存机制,减少对 DynamoDB 和 S3 的频繁访问。例如,使用内存缓存(如 Redis)来缓存常用的数据和图像,提高系统的响应速度。
- 异步处理 :进一步优化异步处理流程,提高工作器的并发处理能力。可以使用多线程或异步编程模型,充分利用系统资源。
- 资源调整 :根据系统的负载情况,动态调整 EC2 实例的数量和配置。使用自动扩展组,根据 CPU 使用率、网络流量等指标自动调整实例数量,确保系统在高负载时能够正常运行。

总结

构建一个容错的图像应用程序需要综合考虑多个方面,包括状态转换的幂等性、数据一致性、系统架构设计、部署管理和故障处理等。通过采用上述的技术和策略,可以确保系统在面对各种故障时能够稳定运行,提供高质量的服务。同时,不断进行性能优化和监控,能够进一步提升系统的性能和可靠性,满足用户的需求。

在实际应用中,还需要根据具体的业务需求和系统规模,灵活调整和优化系统架构和配置,以达到最佳的效果。希望本文提供的内容能够帮助你构建出更加健壮和可靠的图像应用程序。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值