这几年API的作用不断演化,以前API还只是用来做内部系统之间的集成点,但现在API已成为一个公司的核心系统,一个构建于Web和移动端应用之上的核心系统。
当API仅只用来处理后台的任务(例如生成报告),那么性能差点也不是问题。但是如今API慢慢地发展成为连接服务与终端用户的核心纽带。这种关键性的角色变化表明了一个重要的观点:那就是API的性能真的很重要。
如果API数据源响应快,前端的应用程序的设计好点或差点影响不大,要是响应慢如蜗牛,前端的设计再出色也是然并卵。现在我们的客户端应用展示的数据源可能都是来自多个API响应内容的聚合,性能对这种微服务构架来说真的非常重要。
可以毫不夸张的说出色的性能就是你API提供的最好功能。我们知道向目标改进的唯一正确的方法就是找到问题的关键点,或者叫关键路径,并不断迭代测量和调整你的架构系统,直到系统达到预定的目标。对于API来说,测量和提高性能的过程就是负载与压力测试的过程。
本文将重点介绍如何对你的API进行负载压力测试。我们会以一个简单的、未测过的例子开始,然后再添加一个访问控制层,要确保一切都经过严格测试,做好处理真实流量的准备工作。OK,开始吧!
首先我们要明确要测试什么,可以是对你所有的API接口,或者是对单个API接口,或是对需要排除故障或改进的API接口的常规测试。
本文的其部分,我们将使用一个示例API。这是一个棋牌类游戏的Node.js API。它有三个API接口:
/question – 返回一个随机黑牌
/answer – 返回一个随机白牌
/pick – 返回一对随机的问题与答案
你测试用的负荷情况越和真实环境的越类似,你的负载测试就越有用。如果你不知道实际流量有多少或者你不知道负载在所有接口上是否都一致,那么就算你知道你的API可以保持400 请求/秒的吞吐量也没啥鸟用。
所以,你应该先从收集你API的使用数据开始。你可以直接从你的API服务日志或者从其他你在用的应用性能工具(例如New Relic)中获取数据。在对你的API进行第一次测试之前,你应该对以下问题做到心中有数:
(1)每秒请求数的平均吞吐量(Average throughput in requests per second)
(2)峰值吞吐量(您在某段时间内获得的最大流量是多少?)(Peak throughput)
(3)API各接口的吞吐量分布情况(有没有一些接口的流量远超其他接口?)
(4)用户的吞吐量分布情况(少数用户产生大多数的流量,或者是更均匀分布?)
另外还需要考虑的一个关键点是,在测试期间将要模拟的流量会是怎样的,主要考虑点是:
(1)重复负载生成(Repetitive load generation)
(2)模拟流量模式
(3)真实流量
通常我们最好以最简单的方法开始测试,然后逐步演化到更为接近真实环境的测试。我们可以先用重复负载生成来做为API接口的第一个测试,这样不仅可以验证我们的测试环境是否稳定,更重要的是可以让我们找到API能承受的最大吞吐量,这样我们就可以知道API可以达到的性能上限是多少。
找到你的API性能上限值后,你就可以开始考虑如何将你的生成的测试流量塑造得更接近真实环境。使用真实流量来测试是最理想的,但实际 *** 作不太可行。要模拟真实流量比较难,也太花时间。所以我们有一个折中点的方法:先研究你的流量分析数据,并做一个简单的概率模拟。比如你有100个API接口(提示:原文endpoint在这里我译为接口,翻译成端点也可以,不过译成接口感觉更容易理解),你检查了上个月的使用情况,发现80%的流量来自20个接口,其中3个接口占用了50%的流量。那么你就可以创建一个遵循这种概率的请求列表,并提供给你的负载测试工具。这样做就相对快多了,并且它相对比较接近你真实负载,可以显示出你实际环境中可能遇到的问题。
最后,如果你拿到你要测试的API的真实访问日志,你就可以用它们来做最接近客观现实的测试。我们待会儿要讨论的大部分负载测试工具,都是接收一个请求列表作为输入文件。你可以用你的访问日志,稍微做一个格式调整就可以匹配每个测试工具所需的格式。搞定这个你就可以在测试环境中轻松重现你的生产流量。
好了,你清楚了你要测试什么鬼了,准备工作的最后一步就是配置好你的测试环境。你需要一个专用的测试环境。如果你不怕被你老板骂的话,或者比较任性,你也可以直接在你的生产环境中进行性能测试,不过出问题别说哥事先没跟你说清楚哈。
如果您已经设好一个预生产或沙箱环境,并且你的API也在上面运行了,那么你就万事俱备了。因为本文要用示例API,我们会在AWS的服务实例上设置我们的环境。
在我们的例子中,我们使用一个简单的API,不需要从磁盘读取或在内存中保存大型数据集。我们选择Linux C4.large 实例就够了。
注意:我们对比过其他相似处理资源数但内存更大的AWS实例,但实际测试中内存大部分没使用,所以我们选了C4.large
接下来,我们将一个配好的负载测试实例(服务器)运行起来,这只是一个运行模拟测试程序的服务器,它会通过从多个并发连接重复发送请求到我们的API服务器。你需要模拟的负载越高,机器的性能就要求越高。再次,这也是一个CPU密集型工作负载。这里我们选择具有4个虚拟核,16个 ECU的优化处理器的 c4.xlarge AWS服务器
我们选择在相同的可用区内部署所有实例(API服务器与测试服务器在同一个区/机房),这样可以将外部因素对我们测试结果的影响降到最小。
我们有一个沙箱环境来运行我们的API,同时也有另一台服务器准备开始负载测试。如果这是你第一次做性能测试,你一定会想知道什么是最好的方法。在本节中,我们将会分享我们如何选择工具,同时也会介绍一下目前市面上一些公认比较好的工具。
JMeter
在人们意识当中,首当翘楚的估计是 Apache JMeter ,这是一个开源的Java程序,他关键的特性就是提供一个强大而完善的创建测试计划的GUI。测试计划由测试组件组成,测试组件定义了测试的每一个部分,例如:
(1)用来注入负载测试的线程
(2)参数化测试中使用的HTTP请求
(3)可添加侦听器,象widget测试组件那样,可以以不同的方式显示测主式结果
优点:
(1)它是功能性负载测试的最好工具。你可以设定条件来为复杂的用户流建模,还可以创建断言来验证行为。
(2)轻松模拟复杂的http请求,比如请求前的登录验证或文件上传
(3)可扩展性强,有很多社区插件可以修改或扩展内置的行为
(4)开源并且免费
缺点:
(1)GUI学习曲线陡峭,一大堆的选项,在你运行第一个测试之前你得了解大量的概念。
(2)测试高负载时, *** 作步骤很麻烦。你需要先使用GUI工具来生成XML测试计划,然后在非GUI模式下导入测试计划运行测试,因为GUI会消耗掉本用于生成负载的大量资源。你还需要注意所有的侦听器(收集数据与展示测量的组件)哪些要被禁用或启用,因为它们也很耗资源。测试结束后后,你需要将原始结果数据导入GUI以才能查看结果。
(3)如果你的目标是测试一段时间内的持续吞吐量(例如在60秒内每秒请求1000次),那么很难找到正确的并发线程数量和计时器来求出一个比较稳定的数值。
JMeter只是我们在开始测试时用的工具,我们很快开始寻找其他替代方案。原因是,如果你的目标是在Web应用上压力测试复杂的用户流,那么JMeter可能是最好的工具,但如果你只是需要在一些HTTP API接口上进行性能测试,那用它就是杀鸡用牛刀了。
Wrk
Wrk 是一款和传统的 Apache Benchmark (最初用来做Apache服务器的测试工具)非常相似的工具。wrk和ab完全不同于JMeter:
(1)一切都是可以通过命令行工具配置和执行的。
(2)配置少但强大,只有基本生成HTTP负载的必要几项配置
(3)性能强悍
然而,和传统ab工具相比还是有几个优势的地方,主要是:
(1)多线程,所以能利用多核处理器的优势,更容易生成更高的负载
(2)利用Lua脚本很容易进行扩展默认的行为
不好的地方,主要是生成的默认报告在内容与格式上都受到限制(仅文本,无绘图)。当你的目标是找到你的API可以处理的最大负载量,那么wrk是你最佳选择工具。wrk用起来很快就可以上手。
Vegeta
Vegeta 是一款开源命令行工具,但它采用的方式不同于我们以前所见的工具。它专注于如何达到与维持每秒请求数速率。也就是说它侧重在测试支撑每秒X次请求时API会有怎样的服务行为,当你有实际的数据或对你将要达到的峰值流量有个估算时就非常有用,你可以用于验证你的API是否能满足你的需求。
SaaS 工具
正如你之前所看到的,运行一个简单的负载测试需要准备好配置环境。最近有些产品提供负载测试服务。我们试过两个, Loader.io 和 Blazemeter (话外:阿里也有性能测试工具 PTS ,老外估计没试过)。
注意:我们只试了这两个工具的免费版,所以得到的测试结果仅适用于免费版的限定。
Blazemeter
这个产品和我们前面提到的JMeter一样有同样的毛病:如果你只需要用在高负载测试,你需要在GUI界面上创建测试计划,然后在另一个运行非GUI模式的JMeter中导入这些计划。Blazemeter允许你上传JMeter的测试计划到他们的云端并运行,但可惜的是免费版只能设置50个并发用户。
Loader.io
它是一款 SendGrid 出品的简单而强大的云负载测试服务工具。它有你所需要的功能和漂亮的可视报告。 Loader.io 的免费版还是不错的,每秒最多可以有10000次请求的吞吐量,你基本上就可以用它来运行一个真实的负载测试。
我们推荐使用多个工具,以便可以多重检查我们的测试结果,不同的工具有不同的功能与方法,可以更多方面地反映测试结果。
我们先尝试找到我们的API可以承受的最大吞吐量。在这个吞吐量下,我们的API服务达到最大CPU利用率,同时不会返回任何错误或超时。这个吞吐量就可作为我们后面测试要用的每秒请求数。
同样,重要的是要注意到:CPU是限制因素之一,但你也还必须清楚地知道哪些资源会成为你API的性能瓶颈。
我们有必要在API服务器上安装一些工具,以便我们在测试过程中监控资源的利用率情况。我们使用 Keymetrics.io 和 PM2 模块。
我们的Node.js应用运行了一个非常简单的HTTP 服务。Node.js是单线程设计的,但为了利用c4.large AWS实例中提供的双核,我们使用PM2的集群功能来运行应用程序的两个工作进程。
由于我们的API是完全无状态的,所以很容易使用PM2的 核心集群模块(PM2在内部直接使用)。PM2提供的集群功能提供了不错的快捷命令来start/stop/reload应用程序,也可以监控进程。
我们先使用Loader.io对API进行测试。以下是持续30秒,每秒10,000次请求的测试结果,10000次请求是Loader.io免费版中允许的最大吞吐量。
在测试期间,我们观察到API服务器的CPU处理器在测试期间只有几次达到100%的容量。
这表示我们的API可能还可以处理更高的吞吐量。我们接下来通过运行wrk进行第二次测试证实了这一点。我们的目标就是要将我们的API服务器性能推到极限。
wrk -t 4 -c 1000 -d 60 --latency --timeout 3s http://api-server/questions
这里是我们对这个测试做了多次重复测试的结果:
Running 1m test @ http://api-server/question
4 threads and 1000 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 62.23ms 30.85ms 1.35s 99.39%
Req/Sec 4.07k 357.61 5.27k 94.29%
Latency Distribution
50% 60.04ms
75% 63.85ms
90% 64.17ms
99% 75.86ms
972482 requests in 1.00m, 189.89MB read
Requests/sec: 16206.04
Transfer/sec: 3.16MB
结果表明,我们的预感被证实:它达到16,206请求/秒,同时保持合理的延迟,第99百分位只有75.86毫秒。 我们将这作为我们的基准最大吞吐量,因为这一次我们看到了API服务器的最大容量处理能力:
我们刚看到用一个简单的方式来找出你的API可承受的最大流量负载,同时在这过程中我们介绍并讨论了我们看到的一些工具。
请继续关注本文的第二部分,我们将介绍如何控制流量,不要让随随便便一个客户端就可以轻松搞跨您的API。 我们将展示如何通过在架构前端添加代理来确保我们的API的性能不受影响。
本文译自: How to load test &tune performance on your API
视图是VFP所提供的一个强大的数据处理功能,使用视图,不仅可以从数据表中提取一组记录,而且在需要时可以改变记录值,并将更新的结果反映在源数据表中。但在VFP中新建的视图并不是可以更新的,我们需要修改视图的属性才可以使他们可以更新。修改视图属性的方法有两种:
1、在视图设计器中我们只需选中“更新条件”中的“发送SQL更新选项”(在选择这个选项前必须选中一个关键字和至少一个可更新的字段)就可使视图更新。
2、在实际应用中,经常需要临时产生一个视图,这样我们就必须利用程序修改视图的属性使其可以更新,例如:
USE
XJMONTH
CREATE SQL VIEW XJVIEW AS SELECT *FROM XJMONTH WHERE 科室名=KSM
USE
XJVIEW
CURSORSETPROP('KEYFIELDLIST','姓名')
*设置视图与基表对应的关键字段,该关键字段必须是唯一的,否则在发送SQL更新时会出现错误。
CURSORSETPROP('SENDUPDATES',.T.)
*打开SQL更新开关,使视图可以更新基表。
另外,利用CURSORSETPROP函数还可以设置更新字段(默认值是所有字段)、基表别名、更新方式等其他属性,但实际上默认值大部分都能满足需要,只需要制定关键字,打开SQL更新开关就可以了。
使识图可更新的另一个函数是DBSETPROP(),使用DBSETPROP()函数可为当前数据库或当前数据库中的字段、命名连接、表或视图设置属性,但DBSETPROP()函数要求以独占方式使用当前数据库,而CURSORTSETPROP()则可以修改远程视图或临时表的属性,因此还涉及到缓冲访问和更新远程表的控制,另外,两者在语法上也存在一些差别。
如何为 Windows Phone 8 创建基本的 RSS 阅读器适用于:Windows Phone 8 和 Windows Phone Silverlight 8.1 | Windows Phone OS 7.1
您可以通过完成以下步骤创建基本 RSS 阅读器。此处展示的指南和代码示例基于名为 RSS 阅读器示例的代码示例。
本主题包括以下部分。
创建 Windows Phone 应用项目
向整合 DLL 中添加引用
创建 RSS 文本裁边器
更新 XAML 代码
更新代码隐藏文件
相关主题
创建 Windows Phone 应用项目
在 Visual Studio 中创建新的 Windows?0?2Phone 应用 项目。
向整合 DLL 中添加引用
若要使用 SyndicationFeed 类,您必须首先添加 DLL 引用。
向整合 DLL 中添加引用
在 Visual Studio 的“项目”菜单中,选择“添加引用”,然后选择“浏览”选项卡。
导航到“程序文件 (x86)”目录。
导航到 Microsoft SDKs/Silverlight/v4.0/Libraries/Client/。
选择“System.ServiceModel.Syndication.dll”,然后单击“确定”。
注意:
如果在此步骤中您查看到一个询问您是否希望继续的提示,则单击“是”。
创建 RSS 文本裁边器
RSS 源中的说明文本通常包含您可能不希望在 RSS 阅读器中显示的 HTML、编码字符及其他数据。您可以从说明文本创建一个排除 HTML 及其他不需要数据的类,并将该类设置为 XAML 字段的转换器。
创建 RSS 文本裁边器
在“解决方案资源管理器”中,右键单击项目名称,再单击“添加”,然后单击“新建项”。
在“已安装的模板”窗格中,选择“Visual C#”,然后在中间窗格内选择“类”。
命名文件 RssTextTrimmer.cs,然后单击“添加”。
在解决方案资源管理器中,双击 RssTextTrimmer.cs 文件。
添加以下命名空间:
C#
using System.Windows.Data
using System.Globalization
using System.Text.RegularExpressions
更新类定义,以便它可以实现 IValueConverter 接口:
C#
public class RssTextTrimmer : IValueConverter
在 RssTextTrimmer 类中添加以下代码:
C#
// Clean up text fields from each SyndicationItem.
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value == null) return null
int maxLength = 200
int strLength = 0
string fixedString = ""
// Remove HTML tags and newline characters from the text, and decode HTML encoded characters.
// This is a basic method. Additional code would be needed to more thoroughly
// remove certain elements, such as embedded Javascript.
// Remove HTML tags.
fixedString = Regex.Replace(value.ToString(), "<[^>]+>", string.Empty)
// Remove newline characters.
fixedString = fixedString.Replace("\r", "").Replace("\n", "")
// Remove encoded HTML characters.
fixedString = HttpUtility.HtmlDecode(fixedString)
strLength = fixedString.ToString().Length
// Some feed management tools include an image tag in the Description field of an RSS feed,
// so even if the Description field (and thus, the Summary property) is not populated, it could still contain HTML.
// Due to this, after we strip tags from the string, we should return null if there is nothing left in the resulting string.
if (strLength == 0)
{
return null
}
// Truncate the text if it is too long.
else if (strLength >= maxLength)
{
fixedString = fixedString.Substring(0, maxLength)
// Unless we take the next step, the string truncation could occur in the middle of a word.
// Using LastIndexOf we can find the last space character in the string and truncate there.
fixedString = fixedString.Substring(0, fixedString.LastIndexOf(" "))
}
fixedString += "..."
return fixedString
}
// This code sample does not use TwoWay binding, so we do not need to flesh out ConvertBack.
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException()
}
通过在“解决方案资源管理器”中双击“App.xaml”,然后在 <Application.Resources>标记中添加以下代码将 RssTextTrimmer 类设置为转换器:
XAML
<converter:RssTextTrimmer xmlns:converter="clr-namespace:namespace" x:Key="RssTextTrimmer" />
其中,namespace 是项目的命名空间的名称。例如,在 Windows Phone 8 示例的基本 RSS 阅读器中,namespace 被替换为 sdkBasicRSSReaderWP8CS。
更新 XAML 代码
更新 XAML 代码
在“解决方案资源浏览器”中双击 MainPage.xaml 文件。
将 <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"></Grid>替换为下面的 XAML 代码:
XAML
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="0,0,12,0">
<Button Content="Load Feed" Height="72" HorizontalAlignment="Left" Margin="9,6,0,0" Name="loadFeedButton" VerticalAlignment="Top" Width="273" Click="loadFeedButton_Click" />
<ListBox Name="feedListBox" Height="468" HorizontalAlignment="Left" Margin="20,100,0,0" VerticalAlignment="Top" Width="444" ScrollViewer.VerticalScrollBarVisibility="Auto" SelectionChanged="feedListBox_SelectionChanged">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel VerticalAlignment="Top">
<TextBlock TextDecorations="Underline" FontSize="24" Name="feedTitle" TextWrapping="Wrap" Margin="12,0,0,0" HorizontalAlignment="Left" Foreground="{StaticResource PhoneAccentBrush}" Text="{Binding Title.Text, Converter={StaticResource RssTextTrimmer}}" />
<TextBlock Name="feedSummary" TextWrapping="Wrap" Margin="12,0,0,0" Text="{Binding Summary.Text, Converter={StaticResource RssTextTrimmer}}" />
<TextBlock Name="feedPubDate" Foreground="{StaticResource PhoneSubtleBrush}" Margin="12,0,0,10" Text="{Binding PublishDate.DateTime}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Border BorderBrush="{StaticResource PhoneSubtleBrush}" BorderThickness="1" Height="2" HorizontalAlignment="Left" Margin="20,88,0,0" Name="border1" VerticalAlignment="Top" Width="438" />
</Grid>
更新代码隐藏文件
更新代码隐藏文件的步骤
添加以下命名空间:
C#
using System.IO
using System.ServiceModel.Syndication
using System.Xml
using Microsoft.Phone.Tasks
将以下代码添加到 MainPage 类中,放在 MainPage 构造函数后面。
注意:
以下代码中还包括对基本逻辑删除的支持。
C#
// Click handler that runs when the 'Load Feed' or 'Refresh Feed' button is clicked.
private void loadFeedButton_Click(object sender, System.Windows.RoutedEventArgs e)
{
// WebClient is used instead of HttpWebRequest in this code sample because
// the implementation is simpler and easier to use, and we do not need to use
// advanced functionality that HttpWebRequest provides, such as the ability to send headers.
WebClient webClient = new WebClient()
// Subscribe to the DownloadStringCompleted event prior to downloading the RSS feed.
webClient.DownloadStringCompleted += new DownloadStringCompletedEventHandler(webClient_DownloadStringCompleted)
// Download the RSS feed. DownloadStringAsync was used instead of OpenStreamAsync because we do not need
// to leave a stream open, and we will not need to worry about closing the channel.
webClient.DownloadStringAsync(new System.Uri("http://windowsteamblog.com/windows_phone/b/windowsphone/rss.aspx"))
}
// Event handler which runs after the feed is fully downloaded.
private void webClient_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
{
if (e.Error != null)
{
Deployment.Current.Dispatcher.BeginInvoke(() =>
{
// Showing the exact error message is useful for debugging. In a finalized application,
// output a friendly and applicable string to the user instead.
MessageBox.Show(e.Error.Message)
})
}
else
{
// Save the feed into the State property in case the application is tombstoned.
this.State["feed"] = e.Result
UpdateFeedList(e.Result)
}
}
// This method determines whether the user has navigated to the application after the application was tombstoned.
protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
{
// First, check whether the feed is already saved in the page state.
if (this.State.ContainsKey("feed"))
{
// Get the feed again only if the application was tombstoned, which means the ListBox will be empty.
// This is because the OnNavigatedTo method is also called when navigating between pages in your application.
// You would want to rebind only if your application was tombstoned and page state has been lost.
if (feedListBox.Items.Count == 0)
{
UpdateFeedList(State["feed"] as string)
}
}
}
// This method sets up the feed and binds it to our ListBox.
private void UpdateFeedList(string feedXML)
{
// Load the feed into a SyndicationFeed instance.
StringReader stringReader = new StringReader(feedXML)
XmlReader xmlReader = XmlReader.Create(stringReader)
SyndicationFeed feed = SyndicationFeed.Load(xmlReader)
// In Windows Phone OS 7.1 or later versions, WebClient events are raised on the same type of thread they were called upon.
// For example, if WebClient was run on a background thread, the event would be raised on the background thread.
// While WebClient can raise an event on the UI thread if called from the UI thread, a best practice is to always
// use the Dispatcher to update the UI. This keeps the UI thread free from heavy processing.
Deployment.Current.Dispatcher.BeginInvoke(() =>
{
// Bind the list of SyndicationItems to our ListBox.
feedListBox.ItemsSource = feed.Items
loadFeedButton.Content = "Refresh Feed"
})
}
// The SelectionChanged handler for the feed items
private void feedListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
ListBox listBox = sender as ListBox
if (listBox != null &&listBox.SelectedItem != null)
{
// Get the SyndicationItem that was tapped.
SyndicationItem sItem = (SyndicationItem)listBox.SelectedItem
// Set up the page navigation only if a link actually exists in the feed item.
if (sItem.Links.Count >0)
{
// Get the associated URI of the feed item.
Uri uri = sItem.Links.FirstOrDefault().Uri
// Create a new WebBrowserTask Launcher to navigate to the feed item.
// An alternative solution would be to use a WebBrowser control, but WebBrowserTask is simpler to use.
WebBrowserTask webBrowserTask = new WebBrowserTask()
webBrowserTask.Uri = uri
webBrowserTask.Show()
}
}
}
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)