方法调用 所做的传递参数、传回结果都依赖于栈内存。所以Caller 调用者 Callee 被调用者应该同属一个进程,拥有相同的 栈内存,

进程间通信(Inter-Process Communication,IPC)

·管道(Pipe)或者具名管道(Named Pipe)

管道类似于两个进程间的桥梁,可通过管道在进程间传递少量的字符流或字节流。普通管道只用于有亲缘关系的进程(由一个进程启动的另外一个进程)间的通信,具名管道摆脱了普通管道没有名字的限制,除具有管道的所有功能外,它还允许无亲缘关系的进程间的通信。管道典型的应用就是命令行中的“|”操作符,譬如:

ps -ef | grep java

ps与grep都有独立的进程,以上命令就是通过管道操作符“|”将ps命令的标准输出连接到grep命令的标准输入上。

 

·信号(Signal)

信号用于通知目标进程有某种事件发生。除了进程间通信外,进程还可以给进程自身发送信号。信号的典型应用是kill命令,譬如:

kill -9 pid

以上命令即表示由Shell进程向指定PID的进程发送SIGKILL信号。


·信号量(Semaphore)

信号量用于在两个进程之间同步协作手段,它相当于操作系统提供的一个特殊变量,程序可以在上面进行wait()和notify()操作。


·消息队列(Message Queue)

以上三种方式只适合传递少量消息,POSIX标准中定义了可用于进程间数据量较多的通信的消息队列。进程可以向队列添加消息,被赋予读权限的进程还可以从队列消费消息。消息队列克服了信号承载信息量少、管道只能用于无格式字节流以及缓冲区大小受限等缺点,但实时性相对受限。


·共享内存(Shared Memory)

允许多个进程访问同一块公共内存空间,这是效率最高的进程间通信形式。原本每个进程的内存地址空间都是相互隔离的,但操作系统提供了让进程主动创建、映射、分离、控制某一块内存的程序接口。当一块内存被多进程共享时,各个进程往往会与其他通信机制,譬如与信号量结合使用,来达到进程间同步及互斥的协调操作。


·本地套接字接口(IPC Socket)

消息队列与共享内存只适合单机多进程间的通信,套接字接口则是更普适的进程间通信机制,可用于不同机器之间的进程通信。出于效率考虑,当仅限于本机进程间通信时,套接字接口是被优化过的,不会经过网络协议栈,不需要打包拆包、计算校验和、维护序号和应答等操作,只是简单地将应用层数据从一个进程复制到另一个进程,这种进程间通信方式即本地套接字接口(UNIX Domain Socket),又叫作IPC Socket。

 之所以花费那么多篇幅来介绍IPC的手段,是因为最初计算机科学家们的想法,就是将RPC作为IPC的一种特例来看待。

请特别注意最后一种基于套接字接口的通信方式(IPC Socket),它不仅适用于本地相同机器的不同进程间通信,由于Socket是网络栈的统一接口,它也能支持基于网络的跨机进程间通信。

这样做的好处是,由于Socket是各个操作系统都提供的标准接口,完全有可能把远程方法调用的通信细节隐藏在操作系统底层,从应用层面上来看可以做到远程调用与本地的进程间通信在编码上完全一致。事实上,在原始分布式时代的早期确实是奔着这个目标去做的,但这种透明的调用形式反而给程序员带来通信无成本的假象,因而被滥用,以致于显著降低了分布式系统的性能。

 

几十年所有流行过的RPC协议,都不外乎变着花样使用各种手段来解决以下三个基本问题。
1.如何表示数据

这里的数据包括传递给方法的参数以及方法执行后的返回值。每种RPC协议都应该要有对应的序列化协议,譬如:

·ONC RPC的外部数据表示(External Data Representation,XDR)
·CORBA的通用数据表示(Common Data Representation,CDR)
·Java RMI的Java对象序列化流协议(Java Object Serialization Stream Protocol)
·gRPC的Protocol Buffers

·Web Service的XML序列化

·众多轻量级RPC支持的JSON序列化

2.如何传递数据
如何传递数据,准确地说,是指如何通过网络,在两个服务的Endpoint之间相互操作、交换数据。这里“交换数据”通常指的是应用层协议,实际传输一般是基于TCP、UDP等标准的传输层协议来完成的。两个服务交互不是只扔个序列化数据流来表示参数和结果就行,许多在此之外的信息,譬如异常、超时、安全、认证、授权、事务等,都可能产生双方需要交换信息的需求。如果要求足够简单,双方都是HTTP Endpoint,直接使用HTTP协议也是可以的(如JSON-RPC)

3.如何表示方法

不过一旦要考虑不同语言,事情又立刻麻烦起来,每种语言的方法签名都可能有差别,所以“如何表示同一个方法”“如何找到对应的方法”还是需要一个统一的跨语言的标准才行。

 

CORBA没有把握住统一RPC的大好时机,很快另外一个更有希望的机会降临。1998年,XML 1.0发布,并成为万维网联盟(World Wide Web Consortium,W3C)的推荐标准。Web Service采用XML作为远程过程调用的序列化、接口描述、服务发现等所有编码的载体,当时XML是计算机工业最新的银弹,只要是定义为XML的东西几乎都被认为是好的,风头一时无两,连微软自己都主动宣布放弃DCOM,迅速转投Web Service的怀抱。但从技术角度来看,它设计得并不优秀,甚至同样可以说是有显著缺陷的。对于开发者而言,Web Service的一大缺点是它过于严格的数据和接口定义所带来的性能问题。

可是,XML作为一门描述性语言本身信息密度就相对低下,(都不用与二进制协议比,与今天的JSON或YAML比一下就知道了。Web Service又是跨语言的RPC协议,这使得一个简单的字段,为了在不同语言中不会产生歧义,要以XML严谨描述的话,往往需要比原本存储这个字段值多出十几倍、几十倍乃至上百倍的空间。但Web Service还有另外一个缺点:贪婪。“贪婪”是指它希望在一套协议上一揽子解决分布式计算中可能遇到的所有问题,这促使Web Service生出了整个家族的协议。

那些面向透明的、简单的RPC协议,如DCE/RPC、DCOM、Java RMI,要么依赖于操作系统,要么依赖于特定语言,总有一些先天约束;那些面向通用的、普适的RPC协议,如CORBA,就无法逃过使用复杂性的困扰,CORBA烦琐的OMG IDL、ORB都是很好的佐证;而那些意图通过技术手段来屏蔽复杂性的RPC协议,如Web Service,又不免受到性能问题的束缚。简单、普适、高性能这三点,似乎真的很难同时满足。由于一直没有一个同时满足以上三点的“完美RPC协议”出现,所以远程服务器调用这个小小的领域,逐渐进入群雄混战、百家争鸣的战国时代,距离“统一”越来越远,并一直延续至今。

今时今日,任何一款具有生命力的RPC框架,都不再去追求大而全的“完美”,而是以某个具有针对性的特点作为主要的发展方向,举例分析如下。
·朝着面向对象发展

不满足于RPC将面向过程的编码方式带到分布式,希望在分布式系统中也能够进行跨进程的面向对象编程,代表为RMI、.NET Remoting,之前的CORBA和DCOM也可以归入这类。这种方式有一个别名叫作分布式对象(Distributed Object)。

·朝着性能发展

代表为gRPC和Thrift。决定RPC性能的主要因素有两个:序列化效率和信息密度。序列化效率很好理解,序列化输出结果的容量越小,速度越快,效率自然越高;信息密度则取决于协议中有效负载(Payload)所占总传输数据的比例大小,使用传输协议的层次越高,信息密度就越低,SOAP使用XML拙劣的性能表现就是前车之鉴。gRPC和Thrift都有自己优秀的专有序列化器,而传输协议方面,gRPC是基于HTTP/2的,支持多路复用和Header压缩,Thrift则直接基于传输层的TCP协议来实现,省去了应用层协议的额外开销。

-朝着简化发展

代表为JSON-RPC,说要选功能最强、速度最快的RPC可能会很有争议,但选功能弱的、速度慢的,JSON-RPC肯定会是候选人之一。牺牲了功能和效率,换来的是协议的简单轻便,接口与格式都更为通用,尤其适合用于Web浏览器这类一般不会有额外协议支持、额外客户端支持的应用场合。

开发者们终于认可了不同的RPC框架所提供的特性或多或少是有矛盾的,很难有某一种框架能满足所有需求。若要朝着面向对象发展,就注定不会太简单,如建Stub、Skeleton就很烦了,即使由IDL生成也很麻烦;功能多起来,协议就会更复杂,效率一般也会受影响;要简单易用,那很多事情就必须遵循约定而不是自行配置;要重视效率,那就需要采用二进制的序列化器和较底层的传输协议,支持的语言范围容易受限。也正是每一种RPC框架都有不完美的地方,所以才导致不断有新的RPC轮子出现,也决定了在选择框架时,在获得一些利益的同时,要付出另外一些代价。

到了最近几年,RPC框架有明显向更高层次(不仅仅负责调用远程服务,还管理远程服务)与插件化方向发展的趋势,不再追求独立地解决RPC的全部三个问题(表示数据、传递数据、表示方法),而是将一部分功能设计成扩展点,让用户自己选择。尤其是断更多年后重启的Dubbo表现得更为明显。Dubbo默认有自己的传输协议(Dubbo协议),同时也支持其他协议;默认采用Hessian 2作为序列化器,如果你有JSON的需求,可以替换为Fastjson,如果你对性能有更高的追求,可以替换为Kryo、FST、Protocol Buffers等效率更好的序列化器,如果你不想依赖其他组件库,也可以直接使用JDK自带的序列化器。

 

其实,REST无论是在思想上、在概念上,还是在使用范围上,与RPC都不尽相同,充其量只能算是有一些相似,应用会有一部分重合之处,但本质上并不是同一类型的东西。
REST与RPC在思想上差异的核心是抽象的目标不一样,即面向过程的编程思想与面向资源的编程思想两者之间的区别。

REST虽然有宽阔的用武之地,只要支持HTTP就可以用于任何语言之间的交互,不过通常都会以网络没有成为性能瓶颈为使用前提,在需要追求传输效率的场景里,REST提升传输效率的潜力有限,死磕REST又想要好的网络性能,一般不会有好的效果;对追求简化调用的场景——前面提到的浏览器端就属于这一类的典型,众多RPC里也只有JSON-RPC有机会与REST竞争。尽管有着种种不同,REST与RPC还是引发了很频繁的比较与争论,这两种分别面向资源和过程的远程调用方式,就如同当年面向对象与过程的编程思想一样,非得分出高低不可。

 

REST 的关键概念。REST(Representational State Transfer,表征状态转移)

·资源 (Resource)

·表征(Representation)

服务端向浏览器返回的这个HTML就被称为“表征”,你也可以通过其他方式拿到本文的PDF、Markdown、RSS等其他形式的版本,它们同样是一个资源的多种表征。

·状态(State)

当你读完了这篇文章,想看后面是什么内容时,你向服务端发出“给我下一篇文章”的请求。但是“下一篇”是个相对概念,必须依赖“当前你正在阅读的文章是哪一篇”才能正确回应。我们所说的有状态(Stateful)抑或是无状态(Stateless),都是只相对于服务端来说的,服务端要完成“取下一篇”的请求,

要么自己记住用户的状态,如这个用户现在阅读的是哪一篇文章,这称为有状态;要么由客户端来记住状态,在请求的时候明确告诉服务端,如我正在阅读某某文章,现在要读它的下一篇,这称为无状态。

·转移(Transfer)

无论状态是由服务端还是由客户端来提供,“取下一篇文章”这个行为逻辑只能由服务端来提供,因为只有服务端拥有该资源及其表征形式。服务端通过某种方式,把“用户当前阅读的文章”转变成“下一篇文章”,这就被称为“表征状态转移”。

·统一接口(Uniform Interface)

如何能表达出“转移”动作的含义呢?答案是HTTP协议中已经提前约定好了一套“统一接口”,它包括GET、HEAD、POST、PUT、DELETE、TRACE、OPTIONS七种基本操作,任何一个支持HTTP协议的服务器都会遵守这套规定,对特定的URI采取这些操作,服务器就会触发相应的表征状态转移。
·超文本驱动(Hypertext Driven)

尽管表征状态转移是由浏览器主动向服务器发出请求所引发的,但是,我们都清楚这不可能真的是浏览器的主动意图,浏览器是根据用户输入的URI地址来找到网站首页。浏览器作为所有网站的通用的客户端,任何网站的导航(状态转移)行为都不可能是预置于浏览器代码之中,而是由服务器发出的请求响应信息(超文本)来驱动的。这点与其他带有客户端的软件有十分本质的区别,在那些软件中,业务逻辑往往是预置于程序代码之中的,有专门的页面控制器(无论在服务端还是在客户端中)来驱动页面的状态转移。
·自描述消息(Self-Descriptive Message)

由于资源的表征可能存在多种不同形态,在消息中应当有明确的信息来告知客户端该消息的类型以及应如何处理这条消息。一种被广泛采用的自描述方法是在名为“Content-Type”的HTTP Header中标识出互联网媒体类型(MIME type),譬如“Content-Type:application/json;charset=utf-8”说明该资源会以JSON的格式来返回,请使用UTF-8字符集进行处理。


还有一个常见的误区值得注意:Fielding提出REST时所谈论的范围是“架构风格与网络的软件架构设计”(Architectural Styles and Design of Network-based Software Architecture),而不是现在被人们所狭义理解的一种“远程服务设计风格”,这两者的范围差别就好比本书所谈论的话题“软件架构”与本章谈论话题“访问远程服务”的关系那样,前者是后者的一个很大的超集

 

Fielding认为,一套理想的、完全满足REST风格的系统应该满足以下六大原则。
1.客户端与服务端分离(Client-Server)
将用户界面所关注的逻辑和数据存储所关注的逻辑分离开来,有助于提高用户界面的跨平台的可移植性,也越来越受到广大开发者所认可,以前完全基于服务端控制和渲染(如JSF这类)框架的实际用户已甚少,而在服务端进行界面控制(Controller),通过服务端或者客户端的模板渲染引擎来进行界面渲染的框架(如Struts、SpringMVC这类)也受到了颇大冲击。这一点与REST可能关系并不大,前端技术(从ES规范,到语言实现,再到前端框架等)在近年来的高速发展,使得前端表达能力大幅度加强才是真正的幕后推手。


2.无状态(Stateless)
无状态是REST的一条核心原则,部分开发者在做服务接口规划时,觉得REST风格的服务怎么设计都感觉别扭,很可能的一个原因是服务端持有比较重的状态。REST希望服务端不用负责维护状态,每一次从客户端发送的请求中,应包括所有必要的上下文信息,会话信息也由客户端负责保存维护,服务端只依据客户端传递的状态来执行业务处理逻辑,驱动整个应用的状态变迁。客户端承担状态维护职责以后,会产生一些新的问题,譬如身份认证、授权等可信问题,它们都应有针对性的解决方案。
但必须承认的是,目前大多数系统都达不到这个要求,且越复杂、越大型的系统越是如此。服务端无状态可以在分布式计算中获得非常高价值的回报,但大型系统的上下文状态数量完全可能膨胀到客户端无法承受的程度,在服务端的内存、会话、数据库或者缓存等地方持有一定的状态成为一种事实上存在,并将长期存在、被广泛使用的主流方案。


3.可缓存(Cacheability)
无状态服务虽然提升了系统的可见性、可靠性和可伸缩性,但降低了系统的网络性。“降低网络性”的通俗解释是某个功能使用有状态的设计时只需要一次(或少量)请求就能完成,使用无状态的设计时则可能会需要多次请求,或者在请求中带有额外冗余的信息。为了缓解这个矛盾,REST希望软件系统能够如同万维网一样,允许客户端和中间的通信传递者(譬如代理)将部分服务端的应答缓存起来。当然,为了缓存能够正确地运作,服务端的应答中必须直接或者间接地表明本身是否可以进行缓存、可以缓存多长时间,以避免客户端在将来进行请求的时候得到过时的数据。运作良好的缓存机制可以减少客户端、服务端之间的交互,甚至有些场景中可以完全避免交互,这就进一步提高了性能。

4.分层系统(Layered System)
这里所指的分层并不是表示层、服务层、持久层这种意义上的分层,而是指客户端一般不需要知道是否直接连接到了最终的服务器,抑或连接到路径上的中间服务器。中间服务器可以通过负载均衡和共享缓存的机制提高系统的可扩展性,这样也便于缓存、伸缩和安全策略的部署。该原则的典型应用是内容分发网络(Content Distribution Network,CDN)。如果你是通过网站浏览到这篇文章的话,你所发出的请求一般(假设你在中国境内的话)并不是直接访问位于GitHub Pages的源服务器,而是访问了位于国内的CDN服务器,但作为用户,你完全不需要感知到这一点。我们将在第4章讨论如何构建自动、可缓存的分层系统。

5.统一接口(Uniform Interface)
这是REST的另一条核心原则,REST希望开发者面向资源编程,希望软件系统设计的重点放在抽象系统该有哪些资源,而不是抽象系统该有哪些行为(服务)上。这条原则你可以类比计算机中对文件管理的操作来理解,管理文件可能会涉及创建、修改、删除、移动等操作,这些操作数量是可数的,而且对所有文件都是固定、统一的。如果面向资源来设计系统,同样会具有类似的操作特征,由于REST并没有设计新的协议,所以这些操作都借用了HTTP协议中固有的操作命令来完成。

但是,已经有一个基本清晰的结论是:面向资源编程的抽象程度通常更高。抽象程度高带来的坏处是距离人类的思维方式往往会更远,而好处是通用程度往往会更好。\譬如,对于几乎每个系统都有的登录和注销功能,如果你理解成登录对应于login()服务,注销对应于logout()服务这样两个独立服务,这是“符合人类思维”的;如果你理解成登录是PUT Session,注销是DELETE Session,这样你只需要设计一种“Session资源”即可满足需求,甚至以后对Session的其他需求,如查询登录用户的信息,就是GET Session而已,其他操作如修改用户信息等也都可以被这同一套设计囊括在内,这便是“抽象程度更高”带来的好处。

如果想要在架构设计中合理恰当地利用统一接口,Fielding建议系统应能做到每次请求中都包含资源的ID,所有操作均通过资源ID来进行;建议每个资源都应该是自描述的消息;

6.按需代码(Code-On-Demand)
按需代码被Fielding列为一条可选原则。它是指任何按照客户端(譬如浏览器)的请求,将可执行的软件程序从服务端发送到客户端的技术。按需代码赋予了客户端无须事先知道所有来自服务端的信息应该如何处理、如何运行的宽容度。

 

REST的基本思想是面向资源来抽象问题,它与此前流行的编程思想——面向过程的编程在抽象主体上有本质的差别。在REST提出以前,人们设计分布式系统服务的唯一方案就只有RPC,RPC是将本地的方法调用思路迁移到远程方法调用上,开发者是围绕“远程方法”去设计两个系统间交互的,譬如CORBA、RMI、DCOM,等等。这样做的坏处不仅使“如何在异构系统间表示一个方法”“如何获得接口能够提供的方法清单”成为需要专门协议去解决的问题(RPC的三大基本问题之一),而且对于服务使用者来说,由于服务的每个方法都是完全独立的,他们必须逐个学习才能正确地使用这些方法

 

REST提出以资源为主体的服务设计风格,可以带来不少好处。
·降低服务接口的学习成本

统一接口是REST的重要标志,它将对资源的标准操作都映射到标准的HTTP方法上去,这些方法对于每个资源的用法都是一致的,语义都是类似的,不需要刻意去学习,更不需要有诸如IDL之类的协议存在。
·资源天然具有集合与层次结构

以方法为中心抽象的接口,由于方法是动词,逻辑上决定了每个接口都是互相独立的;但以资源为中心抽象的接口,由于资源是名词,天然就可以产生集合与层次结构。

·REST绑定于HTTP协议

面向资源编程不是必须构筑在HTTP之上,但REST是,这是缺点,也是优点。因为HTTP本来就是面向资源设计的网络协议,纯粹只用HTTP(而不是SOAP over HTTP那样再构筑协议)带来的好处是无须考虑RPC中的Wire Protocol问题,REST将复用HTTP协议中已经定义的概念和相关基础支持来解决问题。HTTP协议已经有效运作了三十年,其相关的技术基础设施已是千锤百炼,无比成熟。而坏处自然是,当你想去考虑那些HTTP不提供的特性时,便会彻底束手无策。

 

不足与争议

不足与争议
1)面向资源的编程思想只适合做CRUD,面向过程、面向对象编程才能处理真正复杂的业务逻辑。
这是遇到最多的一个问题。HTTP的四个最基础的命令POST、GET、PUT和DELETE很容易让人直接联想到CRUD操作,以至于在脑海中自然产生了直接的对应。针对那些比较抽象的场景,如果真不能把HTTP方法映射为资源的所需操作,REST也并非刻板的教条,用户是可以使用自定义方法的,按Google推荐的REST API风格,自定义方法应该放在资源路径末尾,嵌入冒号加自定义动词的后缀。譬如,可以把删除操作映射到标准DELETE方法上,如果还要提供一个恢复删除的API,那它可能会被设计为:

POST /user/user_id/cart/book_id:undelete

如果你不想使用自定义方法,那就设计一个回收站的资源,在那里保留还能被恢复的商品,将恢复删除视为对该资源某个状态值的修改,映射到PUT或者PATCH方法上,这也是一种完全可行的设计。

 

2)REST与HTTP完全绑定,不适合应用于要求高性能传输的场景中。
面向资源编程与协议无关,但是REST(特指Fielding论文中所定义的REST,而不是泛指面向资源的思想)的确依赖着HTTP协议的标准方法、状态码、协议头等各个方面。HTTP并不是传输层协议,它是应用层协议,如果仅将HTTP用于传输是不恰当的。对于需要直接控制传输,如二进制细节、编码形式、报文格式、连接方式等细节的场景,REST确实不合适,这些场景往往存在于服务集群的内部节点之间,这也是之前笔者曾提及的,REST和RPC尽管应用确有所重合,但重合范围的大小就是见仁见智的事情。


3)REST不利于事务支持。
这个问题首先要看你怎么看待“事务(Transaction)”这个概念。如果“事务”指的是数据库那种狭义的刚性ACID事务,那除非完全不持有状态,否则分布式系统本身与此就是有矛盾的(CAP不可兼得),这是分布式的问题而不是REST的问题。如果“事务”是指通过服务协议或架构,在分布式服务中,获得对多个数据同时提交的统一协调能力(2PC/3PC),譬如WS-AtomicTransaction、WS-Coordination这样的功能性协议,REST是不支持的,假如你理解了这样做的代价,仍坚持要这样做的话,Web Service是比较好的选择。如果“事务”只是指希望保障数据的最终一致性,说明你已经放弃刚性事务了,这才是分布式系统中的正常交互方式,使用REST肯定不会有什么阻碍,更谈不上“不利于”。当然,对此REST也并没有什么帮助,这完全取决于你的系统的事务设计,我们在第3章中再详细讨论。

 

4)REST没有传输可靠性支持。
是的,并没有。在HTTP中发送一个请求,你通常会收到一个与之相对的响应,譬如HTTP/1.1 200 OK或者HTTP/1.1 404 Not Found等。但如果你没有收到任何响应,那就无法确定消息是没有发送出去,抑或是没有从服务端返回,这其中的关键差别是服务端是否被触发了某些处理?应对传输可靠性最简单粗暴的做法是把消息再重发一遍。这种简单处理能够成立的前提是服务应具有幂等性(Idempotency),即服务被重复执行多次的效果与执行一次是相等的。HTTP协议要求GET、PUT和DELETE应具有幂等性,我们把REST服务映射到这些方法时,也应当保证幂等性。对于POST的重复提交,浏览器会出现相应警告,如Chrome中“确认重新提交表单”的提示,对于服务端,就应该做预校验,如果发现可能重复,则返回HTTP/1.1 425 Too Early。

 

5)REST缺乏对资源进行“部分”和“批量”处理的能力。

如譬如你仅仅想获得某个用户的姓名,如果是RPC风格,可以设计一个“getUsernameById”的服务,返回一个字符串,尽管这种服务的通用性实在称不上“设计”二字,但确实可以工作;而果是REST风格,你将向服务端请求整个用户对象,然后丢弃掉返回结果中该用户除用户名外的其他属性,这便是一种过度获取(Overfetching)。REST的应对手段是通过位于中间节点或客户端的缓存来解决这种问题,但此缺陷的本质是由于HTTP协议完全没有对请求资源的结构化描述能力。

与此相对的缺陷是对资源的批量操作的支持,有时候我们不得不为此而专门设计一些抽象的资源才能应对。譬如你准备给某个用户的名字增加一个“VIP”前缀,提交一个PUT请求修改这个用户的名称即可,而你要给1000个用户加VIP前缀时,如果真的去调用1000次PUT,浏览器会回应HTTP/1.1 429 Too Many Requests。此时,你就不得不先创建一个任务资源(如名为“VIP-Modify-Task”),把1000个用户的ID交给这个任务,然后驱动任务进入执行状态。