构建高性能和高弹性 WebSphere eXtreme Scale 应用程序的原则和最佳实践
2010-07-23 00:00:00 来源:WEB开发网简介
数据是用于管理、挖掘和操作数据的所有计算系统的核心元素。在 Internet 年代,应用程序不仅要求即时访问数据,通常还以压倒性的近乎同步的请求尝试该访问。尽管数据库技术有了很大提高,集中式数据存储对这种需求和响应能力的应用程序来说还是存在问题。
IBM WebSphere eXtreme Scale 为高容量和高 SLA(服务级别协议)应用程序提供集中式数据访问选择。WebSphere eXtreme Scale 通过缓存技术将数据拉近应用程序。将数据移近应用程序可获得以下优势:
本地数据可改善应用程序的性能。 这既包括缓存数据使其接近应用程序,又包括复制靠近(分区)数据的应用程序逻辑,以实现并行处理。
为频繁访问的数据提供缓存可以节省时间或降低数据库的争用和总体请求容量。 这转而降低数据库级的硬件和许可成本,且可为共享该资源的应用程序提高数据库总体响应能力。
将数据拉近应用程序可提高可用性。WebSphere eXtreme Scale 还提供了复制整个环境中的数据的特性,从而进一步提高弹性。
与访问基于 SQL 的数据相比,以最终的本机应用程序形式(对象)缓存数据可减小路径长度。
缓存复杂业务逻辑的结果可加快后续调用,并降低总体解决方案成本。
WebSphere eXtreme Scale 是一种通用、高速的缓存解决方案,可在各种不同的设计中予以配置和使用。不过,您不能盲目地使用 WebSphere eXtreme Scale 提供的 API,并想当然地认为它会减轻数据库的工作重负,使您的应用程序更快地运行。作为提高应用程序性能的一种策略,缓存应当被明智谨慎地应用。同样地,您不能想当然地认为您的应用程序在遇到硬件故障时具有弹性,除非您为此筹备了有意识的计划。本文考查了大量最佳实践,帮助您构建高性能和高弹性的 WebSphere eXtreme Scale 应用程序。
查询的合理使用
当您想知道如何在一个缓存解决方案中使用 WebSphere eXtreme Scale 时,需要考虑的一个首要设计原则很简洁但很重要:
WebSphere eXtreme Scale 不是一个关系数据库。
然而,很多首次 WebSphere eXtreme Scale 实现都会犯假设这样一个通病。但怎么会有这种想法产生呢?有时它是因未考虑 WebSphere eXtreme Scale API 不同方面的性能影响而产生的。
WebSphere eXtreme Scale 有两个不同的 API,用于定义将对象放入缓存和从缓存中获取对象的方式:
第一个是 Map API,它基于 java.util.Map API。在使用 Map API 时,您要有效对待网格中的对象,如同它们是本地哈希映射中的对象一样。使用 insert()、put() 和 get() 等方法将对象放到网格中并从网格中检索它们。有了 Map API,网格的最合理用法就变得一清二楚:您仅需在合适的键处将对象放入,并使用同一个键查询对象。当考虑第二个可用 WebSphere eXtreme Scale API,即 EntityManager API 时,就会产生数据库混乱。
EntityManager API 是仿照 JPA (Java™ Persistence API) 实体模型建模的。使用 EntityManager API 时,会使用注释描述置入 WebSphere eXtreme Scale 缓存中的实体。使用 EntityManager.persist() 方法将对象存储到网格中,与对 JPA 所做的处理一样。不过,与 JPA 的相似性有时致使开发人员做出一些效率低下的选择,从而导致对产品的次佳使用。您可以使用 find() 方法或查询方法来查询实体,前者根据对象的键检索对象,而后者根据一系列属性值查询对象。
首次使用 WebSphere eXtreme Scale 的用户可能过多依赖于 WebSphere eXtreme Scale EntityManager API 的查询功能,特别是在将 WebSphere eXtreme Scale 纳入专为数据库访问定制的现有应用程序时。WebSphere eXtreme Scale 基本上是一个缓存提供者。查询功能是产品的一个便捷特性,但它不受高端数据库中所有高级查询优化器的支持。因此,WebSphere eXtreme Scale 最精于根据对象的键查找对象,而不是根据查询定位该对象。这又涉及到另一个主要原则:
到网格中任何对象的主要访问方式应该是通过 Map API 或使用对象主键的 EntityManager find() 方法。
这个简单的设计原则能消除设计 WebSphere eXtreme Scale 应用程序时的许多性能问题。应用程序在这里也可以使用 WebSphere eXtreme Scale 的查询特性,但对于高性能路径或全部高性能应用程序,推荐的访问方式是 Map API 或 find() 操作。(有关查询的索引和其他优化 — 如果您的应用程序用得到 — 将在 后面 介绍。)
总而言之,您需要使用缓存来优化数据的高容量和高访问频率。要实现缓存的性能优势以及减少到中央数据库的流量,最好的方式就是最大化缓存的命中率;在 WebSphere eXtreme Scale 中,尤其包括了近缓存(near cache)(即与客户端在同一个内存空间)。因此,要利用该优势,必须经常访问缓存中的数据。图 1 显示了为实现高性能访问的一个对象网格解决方案概貌。
图 1. ObjectGrid 组件
WebSphere eXtreme Scale 中的一个 shard 是置于容器中的数据的一个分区。shard 有主 shard,也有副本 shard。WebSphere eXtreme Scale 通过将复制的数据放在独立的服务器中来提高数据的可用性。
在该场景中,对 ObjectGrid API (1) 的一个客户端调用首先引起对近缓存 (2) 的搜索。如果未找到结果,ObjectGrid 会定位合适的 ObjectGrid 服务器,以及该服务器中应包含结果 (3) 的这个 shard。如果它不存在,那么结果可能会是一个正被调用的加载程序,它会从一个后端数据存储 (4) 加载值。为了量化这个过程,接下来需要检查 WebSphere eXtreme Scale 缓存解决方案各方面的作用。不过在此之前,我们要先讨论缓存设计的一些一般原则。
寻找正确的缓存时效性
使用缓存是对时间与内存大小之间的一个平衡行为。缓存的一个关键原则是用内存换取其他性能,比如更好的响应时间和/或增加的弹性,和/或其他系统中降低的成本占用。因此,管理缓存中对象的生命周期很重要。缓存中对象的生命周期由跨越一组数据访问的缓存中的数据一致性决定。
对于每个应用程序,其数据都有一个可接受的 “过时” 程度。由于其实现,WebSphere eXtreme Scale 从不允许在其缓存中有同一数据的不同版本。它保证每个缓存中只存在每个对象的同一版本,除非使用了一个近缓存。这使 WXS 甚至在缓存读/写数据时都很有用。例如,您不会在批处理作业运行时更改其中使用的数据集。不过,在一个交互式应用程序中,对数据的更改可能更动态;如果用户已经登录了很长时间,那么他会希望在发生数据变更时有所反映。因此在第一种情况下,缓存中对象的最短生命周期就是批处理作业的生命周期。在第二种情况下,它根据用户希望多快看到数据变更而有所不同。不管在哪种情况下,您都应该尽量选择简单的收回(eviction)策略,比如生存时间(time- to-live)。在上述两种情况下,这可简单实现,只需选择一个适合正开发的应用程序需求的时间;对于批处理应用程序来说时间较长(例如,比最长的批处理作业稍长一些),对于在线应用程序来说时间稍短一些。当然,由多个用户或所有批处理作业以只读方式共享的元素能赢得一个更长的缓存生命周期。 WebSphere eXtreme Scale 还采用各种方式来使变更数据的处理更智能,能够在发生实际变更时做出反应。
不过,在考虑近缓存的情况下,究竟数据是否过时这个问题会有所不同。记住,近缓存对于每个 WebSphere eXtreme Scale 客户端都是独特的,不像主 ObjectGrid 缓存一样被共享。鉴于此原因,近缓存中的数据相互之间可以不同步,与主 ObjectGrid 缓存也可以不同步。因此,您的选择通常有两个,要么为近缓存中的数据设置一个非常短的生存时间(这会降低效用),要么完全不使用近缓存而仅依赖于主 ObjectGrid 缓存(这不会涉及过时性问题)。
近缓存也只有在它含有大量足够空间可用的时候才有用。如果一个 WebSphere eXtreme Scale 缓存在缓存 200 GB 的数据,那么近缓存可能就因为客户端没有足够的内存而不容纳该数据的子集来提高利用率。
最后,当考虑到对过时性的设计和容差时,您需要确定哪些实际记录系统在总应用程序设计中。如果 ObjectGrid 是您的记录系统,那么您可以使用后台写(write-behind)数据库更新或将缓存设置为一个连续写入(write-through)缓存。这样做的好处就是缓存中绝不会有过时数据,因为缓存是数据的权威来源。另一方面,如果数据库是记录系统,那么您可以使用开放式锁定或 ObjectGrid 的内置 SCN 支持来检测和处理缓存中的过时数据。
ObjectGrid 缓存方程式
现在您既然已经理解了缓存设计的一般原则,就可以开始考虑该方程式:
Tavg = Nh × Tn + (1-Nh) × (Rh × Tr + (1-Rh) × Td)
该方程式中的每个元素都被用来计算网格处理任意请求的平均时间。让我们轮流看一下这些元素,然后讨论在 ObjectGrid 设计中每个元素的效果。
Nh 是方程式中第一个主要因子。它代表近缓存访问率。近缓存 是位于 ObjectGrid 客户端内的一个小缓存 — 因此是可以最快访问的缓存,因为使用它不必包含对缓存的任何进程外调用,包括那些可能引发网络访问的调用。
Tn 是方程式中第二个主要因子。这是从近缓存中检索对象所需的时间。事实上,这很低;多数情况下少于 1ms,所以您可以简化数值计算,直接将其看作 1ms 的常量。这与图 1 中的第 2 个项目相对应。
Rh 是远缓存的访问率。远缓存是位于网格中的缓存。访问率代表在远缓存中多久能找到一个对象。
Tr 是从远缓存中检索对象所用的时间。它取决于:
进程外调用的序列化开销。
网络延迟。
WebSphere eXtreme Scale 处理开销。
处理双方请求的处理器核心的时钟速度。
对象的大小影响这些因素的作用。而且,频繁进行进程外调用可能增加 JVM 的垃圾回收开销,因此在度量中也应寻找和考虑该因素。当然,度量意指测试,且这些指标应该在测试中根据试验测定,因为时序随设计的不同而有所不同。它们还受网络状况、WebSphere eXtreme Scale 配置和服务器处理速度的影响。这与图 1 中的第 3 行相对应。
Td 是从数据库中检索对象所用的时间。它也应在测试中根据试验测定。这与图 1 中的第 4 个项目相对应。
因此,为了解其用法,我们将数字代入方程式中。
假定在某个 WebSphere eXtreme Scale 应用程序中,您确定了对近缓存的访问率大约为 70%;例如,在该 WebSphere eXtreme Scale 客户端中,您将使用 70% 的时间访问最近从远缓存中获取的对象。同样地,假设您对远缓存的访问率是 80%;例如,与将新对象拉入网格中相比,您将使用 80% 的时间访问从数据库取出并放到远缓存中的对象。最后,您还可以通过试验将 Tr 确定为 20ms,且将 Td 确定为 200ms。代入这些数字之后的方程式为:
Tavg = (0.7 * 1ms + (1-0.7) * (0.8 * 20ms + (1-0.8) * 200ms) = 17.5ms
我们可以看一下,它在多大程度上取决于访问率的系数;例如,只将近缓存访问率小幅增加到 80%,平均响应时间就变为 12ms — 提高了 30%。不过,同样需要意识到,70% 的访问率对应的响应时间是 1ms。但是,较大的极端值(200ms)与平均值很不对称,因此在整体描述应用程序的行为时必须将所有因素考虑在内,这很重要;它真正归结于您的用户希望且能容许有什么样的性能,同时从平均响应时间和缓存在 “大部分时候” 的效果等方面考虑。
从缓存方程式得出的最佳实践
我们已经了解了 WebSphere eXtreme Scale 解决方案的每个组成的作用,现在需要应用一些从该方程式中各因子推断得出的最佳实践:
仔细考虑一下使用近缓存的优缺点。这里的问题是您可能在应用程序中有两个相互矛盾的目标。一个目标就是在设计应用程序时尽量合理地使 Nh 接近 100%, 这将使其余组成微不足道,极大地提高应用程序的总体速度。具体方法就是选择缓存中的对象,从而使其展示良好的 引用局部性。不过,另一个目标可能就是避免之前讨论的缓存过时性或同步问题。如果您的应用程序需求允许使用近缓存,那么您就应该使用它,若不然,您就需要继续寻找能提高总体性能的可行方法。
接下来,优化 Rh,您可以使用预加载策略或预测性加载策略提高在远缓存中找到对象的几率。。预加载是一种在启动时将整个对象集加载到缓存中的方法。而当您加载一个对象(比如缓存缺失之后)然后推测同时还需要哪些其他相关对象并加载它们时,则需要用到预测性加载策略。下一节 有关预加载与按需加载的描述将有助于您理解这两种方法的权衡。
通过减少网络上发送的信息量优化 Tr。您可以使用一个自定义序列化方法减少网络序列化/反序列化开销。您还可以优化对象设计使其去除不必要的字段,从而减小所返回的对象的大小(以及网络传输时间)。您可能还能够使用二级缓存等方法同时提高远缓存访问率和检索率。
Td 通过使用标准数据库调优方法获得优化。这是总体应用程序调优流程的一部分,您一定不会忘记。
选择预加载或按需加载
WebSphere eXtreme Scale 能实现比过去更大的缓存容量。70 到 140 GB 之间的基于内存的缓存是 WebSphere eXtreme Scale 用户常使用的。它的实现方式是将来自多个 JVM 的内存聚合以形成全局缓存。这也意味着,可以在多个 JVM 上分布较小的缓存,从而在需要每个 JVM 缓存所有数据时较以前减少每个 JVM 的内存量。一般来说,与传统缓存相比,WebSphere eXtreme Scale 用户预期对每个 JVM 使用更少的内存。
这就是说,您应该尽量按需使用内存。工作集是指在特定时间段内应用程序将访问的最小结果集,以使应用程序更高效地工作。工作集的大小和组成由应用程序的设计决定;在重复多个操作的批处理应用程序中,工作集可能比没有可简单预测访问模式的在线应用程序更大(且寿命更长)。
如果您可以及早识别一个工作集,且它适合可用内存,那么最简单的策略通常就是预加载工作集。该策略特别适用于这种情况,即工作集中表格尺寸太小,因而逐项加载表格花费的时间远远大于一次加载所有表格所用的时间。
如果您不能及早识别一个工作集,则延迟加载就是填补缓存的下一个最佳选择。延迟加载策略的构想是,如果将一个数据加载到了缓存中,就有可能再次用到它。WebSphere eXtreme Scale 直接通过其加载程序工具支持延迟加载。该策略的一个变体就是预测性加载策略,它在任何时候有针对特定数据的请求时加载相关数据。例如,如果一个缓存收到一个客户数据请求,该请求基于目前不在缓存中的一个特定客户编号,那么您可能认为客户帐户缓存也会从对该客户帐户数据的预先填充中获益。
需要注意,一个延迟加载的缓存不适合查询,这通常是与缓存有关的一个不争的事实(不管是 WebSphere eXtreme Scale 还是另一类型):您只能针对完整的缓存(有完整的数据集)执行查询。如果缓存不完整,您需要返回到数据库执行查询。
如果您费力去预加载数据集,应该同时将其复制,以避免出错时需要重新加载它。WebSphere eXtreme Scale 也支持为获得弹性而复制延迟加载的数据。这在以下情况下很有用:
数据不经常地相对于请求容量发生变化。例如,在一个 eCommerce 站点中,受欢迎的项目可能会有每小时上千次的检索量,而商家可能一天只更新目录一次。
如果数据请求容量整体较高,复制或分布数据可以潜在地降低容量争用。
如果对源数据库的访问不可靠,在缓存层争取弹性可能会提高读密集型应用程序的总体稳定性。
其他设计原则
然而,并非所有的 WebSphere eXtreme Scale 最佳实践都可从缓存方程式中获得。在 WebSphere eXtreme Scale 解决方案的开发过程中有大量元素可用于处理数据检索、数据分布,以及需要考虑的其他方面的问题。
选择正确的方式来查找数据
在设计一个网格时,您需要仔细选择分区键。为了有效访问大小为 >~1 GB 的数据,分区应沿着合理的键进行。如果根据两个或多个键查询一个对象,进行分区所依据的最合理的键将是最常用的键。使用其他键进行查询时,考虑使用全局反向索引。
一个全局反向索引仅仅是一个设计,其中创建的映射的键是搜索关键词,且其值是包含属性与搜索关键词的键列表。因此,一个索引查询会产生一个返回键列表的 Map.get(value)。然后该键列表可以使用 WXSUtils.getAll(一个按源代码提供的 WebSphere eXtreme Scale 帮助库,包括常见任务的许多常规帮助)快速大批量获取数据。您现在有了更好的解决方案。该系统的吞吐量比并行搜索实现要好很多。每个索引查询使一个 PRC 只对应一个数据库。因此,一个数据库可以执行 N 个查询。M 个服务器可以执行 M * N 个查询,从吞吐量角度来看,您现在的索引查询有更好的响应时间和线性伸缩性。
最后一种情况是使用 ObjectGrid 查询,仅当处于某种原因不能应用反向索引或应用它会使设计更复杂时,才考虑使用这种查询。在这种情况下,您应该使用合适的查询索引(下一节)提高查询的性能。
应用查询索引
如同在关系数据库中一样,WebSphere eXtreme Scale 支持使用索引提高查询速度。索引名称与对应的编入索引的属性名称相同,且通常是一种 MapRangeIndex 索引类型。可以使用 Entity 类中的 @index 注释或 ObjectGrid 描述符 XML 文件中的 HashIndex 插件配置指定每个索引。清单 1 显示了如何在一个 objectGrid.xml 文件中指定一个索引配置。
清单 1. 在 objectgrid.xml 中实现索引
<bean id="MapIndexPlugin"
className="com.ibm.websphere.objectgrid.plugins.index.HashIndex">
<property name="Name" type="java.lang.String" value="SMTRSET"
description="index name" />
<property name="RangeIndex"
type="boolean" value="true" description="true for MapRangeIndex" />
<property name="AttributeName" type="java.lang.String"
value="SMTRSET" description="attribute name" />
</bean>
为展示应用索引后的影响,我们对含有约 680,000 个存储在 ObjectGrid 6.1.0.3 单一分区中的对象执行了一些测试,测试目标是非主键查询性能,包括非索引查询、索引查询和 MapRangeIndex 查询。在该测试中,对象返回的数目在所有情况下都是 10。(该测试在一个 Intel Core Duo T7700 (2.40 GHz) 上执行,它含有 2GByte 内存,JVM Heap 设为 768 MB,且 HDD 为 100GB,7200rpm。)
在该测试中,我们创建了一个名为 OgisCustomer 的实体,它有包括主键(SRCSEQ)在内的 4 个成员属性。其中一个成员属性(SMTRSET)设定为含有一个如上面 ObjectGrid.xml 中所示的一个索引。清单 2 显示了描述属性的 entity.xml 文件。
清单 2. entity.xml
<entity class-name="sample.entity.OgisCustomer" name="OgisCustomer" access="FIELD"
schemaRoot="true" >
<description>OGIS CUSTOMER Class</description>
<attributes>
<id name="SRECSEQ"/>
<basic name="SMTRSET"/>
<basic name="SGASSYOKY"/>
<basic name="SSHRKY"/>
</attributes>
</entity>
我们的 EntityManager 查询性能试验表明,在这种情况下,您只需添加一个索引配置就可以获得比之前快 2000 倍的结果。
查询类型 | 性能结果(msec) |
非索引查询 | 20414 |
索引查询 | 10 |
MapRangeIndex 查询 | 15 |
弹性最佳实践
目前为止呈现的最佳实践一般都是用于提高 WebSphere eXtreme Scale 应用程序的性能。不过,也有其他最佳实践与结果应用程序的整体弹性相关。
何时使用副本 shards
在应用程序中要考虑的最简单的问题是,网格弹性是否是应用程序中的一个问题。为便于理解,您需要考虑网格中数据的价值。毕竟在多数网格中,网格是一个缓存,而不是 “记录系统”。因此,如果缓存缺失,总是可以还原缓存中的值。问题在于,重新创建缓存的成本(时间或 CPU)太高就会导致在缓存失败时应用程序不能满足其服务级别协议。这就又涉及到下一个原则:
如果缓存中的数据值得预加载,那么就值得在缓存中创建数据副本。
虽然这不是值得创建副本的惟一情况,但绝对是主要情况。在考虑预加载时,节省试图重新创建缓存的时间和精力将是一个设计目标。
这里的另一个因素是,许多用户使用网格是因为数据库跟不上加载。如果出于日常维护对网格的一部分加以循环利用,在没有副本的情况下就会致使一些缓存数据丢失。数据的丢失可能造成数据库负荷过多,因为缓存不再充当数据子集的缓冲区。因此,使用缓存帮助数据库减负的系统通常需要通过复制来避免这些情况。
实现区域配置
一般来说,WebSphere eXtreme Scale 不会将一个主 shard 和副本 shard 放在有相同 IP 地址的 JVM 上。但这不一定表示,它会确保每种情况下都高度可用,特别是在刀片环境中。如果您有两个不同的刀片机箱,WebSphere eXtreme Scale 的区域功能允许您将主 shard 和副本 shard 放在不同的机箱中。您可以使用 IBM WebSphere Virtual Enterprise 或非 WebSphere Virtual Enterprise 环境下支持的区域特性启动 WebSphere eXtreme Scale 服务器。除此之外,如果您的 WebSphere eXtreme Scale 应用程序在 IBM WebSphere Application Server Network Deployment 上运行,您可以在 objectGridDeployment 配置文件中指定区域元数据,使其更加高度可用。
有多种方式可实现 WebSphere eXtreme Scale wiki 中描述的区域,不过无论在哪种情况下都要显式指定 <zone name>。物理节点与区域之间的关联要遵从命名规定 eplicationZoneZONENAME。如果您的 WebSphere eXtreme Scale 安装在一个 WebSphere Application Server 或 WebSphere Virtual Enterprise 单元上,ZONENAME 的名称必须与单元中指定的 NodeGroup 完全相同。
在清单 3 中,主 shard 和副本 shard 分别位于两个节点组中,即 XNodeG 和 YNodeG。在存在两个刀片机箱的情况下,每个刀片上都应指定每个节点组。(该规则也完全适用于 WebSphere Virtual Enterprise。)
清单 3. 使用区域的 Shard 分布
<?xml version="1.0" encoding="UTF-8"?>
<deploymentPolicy xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://ibm.com/ws/objectgrid/deploymentPolicy
../deploymentPolicy.xsd"
xmlns="http://ibm.com/ws/objectgrid/deploymentPolicy">
<objectgridDeployment objectgridName="LDAP_OBJECT_GRID" >
<mapSet name="mapSet1" numberOfPartitions="10" minSyncReplicas="0"
maxSyncReplicas="0" maxAsyncReplicas="1" developmentMode="false"
numInitialContainers="2"
> <map ref="PEOPLE_USER_TYPE_MAP" />
<map ref="SPECIAL_USER_TYPE_MAP" /> <map ref="ORG_GROUP_TYPE_MAP" />
<map ref="BIZ_GROUP_TYPE_MAP" />
<zoneMetadata> <shardMapping shard="P" zoneRuleRef="stripeZone"/>
<shardMapping shard="A" zoneRuleRef="stripeZone"/>
<zoneRule name="stripeZone" exclusivePlacement="true" >
<zone name="ReplicationZoneXNodeG" />
<zone name="ReplicationZoneYNodeG" />
</zoneRule> </zoneMetadata>
</mapSet>
</objectgridDeployment>
</deploymentPolicy
结束语
本文考查了大量用于提高 IBM WebSphere Extreme Scale 性能和弹性的最佳实践,以及有助于更好地理解 WebSphere Extreme Scale 产品的原则。这些信息能够帮助您开发和优化自己的 WebSphere eXtreme Scale 应用程序,并避免在您的环境中应用该产品时出现问题和失误。
- ››构建Windows 8风格应用23-App Bar概述及使用规范
- ››构建域名服务器(DNS)
- ››构建Android平台Google Map应用
- ››构建WinForm 通用速选(全选、反选、清空)组件
- ››构建Wordpress网站首选的5家国外主机
- ››构建高性能和高弹性 WebSphere eXtreme Scale 应用...
- ››构建前端UI组件的新思路
- ››构建 Android 开发环境
- ››构建 pureXML 和 JSON 应用程序,第 3 部分: 为 p...
- ››构建 ESB 中介来将分隔文件转换为服务调用
- ››构建一个 Twitter Web 应用程序
- ››构建基于 CDT 的编辑器,第 1 部分: C/C++ 开发工...
更多精彩
赞助商链接