Download - Optymalizacja aplikacji ASP.NET
Optymalizacja aplikacji ASP.NET
Skalowalność – ilu użytkowników może pracować jednocześnie? Aplikacja może obsługiwać tysiące użytkowników, ale być wolna Zmiany w sprzęcie, infrastrukturze sieciowej, itp. (istotna architektura!)
Wydajność – jak szybko ładują się strony? Zasada 8 sekund Liczy się odczuwalna wydajność (np. reklamy dopiero na końcu)
To także: Szybkość wprowadzania poprawek i zmian Łatwość wdrażania aplikacji Proces wytwórczy …
Architektura Bing vs strony domowej (można przesadzić)
Skalowalność i wydajność
Lokalny cache przeglądarki DNS – cache / zapytanie Proxy – visible / transparent Cache serwera
System - http.sys IIS ASP.NET (dane w ram / z dysku)
Cache SQL Server Dyski – SSD / zwykłe / cache…
Komponenty wpływające na wydajność
Odczuwalna wydajność Kolejność ładowania, AJAX
Mniej żądań do serwera Walidacja po stronie klienta, bundling, itp.
Cache na wszystkich warstwach Minimalizacja blokujących wywołań
Synchroniczne operacje zapisu do bazy, zewnętrzne usługi, itp.
Optymalizacja I/O (dyski, itp.) Także partycjonowanie / sharding, itp.
Najważniejsze zasady przy optymalizacji
Wydajność po stronie klienta
Maximum Transfer Unit (MTU) - rozmiar okna odpowiedzi Od 500 do 1500 bajtów Dużo komunikatów SYN-ACK
TCP Slow Start (RFC 5681) Bardzo kosztowne nawiązywanie połączenia Długi okres między pierwszym pakietem a kolejnym Zapobiega przeciążeniu sieci
Jak najmniej nowych połączeń (np. żądania plików) HTTP Keep-Alives – domyślnie w IIS 120 sekund
Np. przy długich formularzach można zwiększyć (ostrożnie)
Protokół TCP
TCP Slow Start - przykład
IE Developer tools (F12) Żółte – slow start + pierwszy pakiet odpowiedzi Niebieski – reszta odpowiedzi
Jeśli <img> było w pierwszym pakiecie
<head> Przeglądarka nie wyświetli nic przed pobraniem całego
nagłówka „Lookahead” – ograniczenia
Jak najwięcej w <body> Nawet <link> i <style> (niezgodne ze specyfikacją, ale
działa) Kolejność
Np. duży baner reklamowy na górze strony – w kodzie lepiej niżej
Struktura strony
Ustawiać width i height dla <img>!
Późne ładowanie - placeholder
<img id="myimg" width="50" height="50" /> <!-- Klasycznie -->
<script type="text/javascript"> document.getElementById("myimg").src = "myimage.jpg"; </script>
<img id="Img1" width="50" height="50" />
<!-- w jQuery -->
<script type="text/javascript"> $("#myimg").attr("src", "myimage.jpg"); </script>
Późne ładowanie – preloading
<!-- Preloading - na górze strony lub w onload (jeśli może być później) --> <script type="text/javascript"> var myimg = new Image(); myimg.src = "myimage.jpg"; </script> <!-- Niżej na stronie, będzie załadowane z cache (dla rolloverów) --> <img src="myimage.jpg" width="50" height="50" />
Wielkość liter W Unix – system plików case sensitive Niektóre serwery cache’ują oddzielnie
Może spowodować wysłanie dwóch żądań
Można dołączyć moduł http, który to poprawia Referencje do tej samej domeny
Przekierowanie z domena.com/gfx.jpg na www.domena.com/gfx.jpg
Cache i URL
<img src="myimage.jpg" width="50" height="50" /> <img src="myimage.JPG" width="50" height="50" />
Przeglądarki mają do 6 równoległych połączeń dla domeny Przed IE8 – tylko 2!
Można podzielić pliki na kilka domen img.mojadomena.com, css.mojadomena.com, itp. Aliasy lub inne usługi cookieless (np. Azure Blob)
Przetwarzanie żądań
<img src="q1.gif" height="16" width="16" /><img src="q2.gif" height="16" width="16" /><img src="q3.gif" height="16" width="16" /><img src="q4.gif" height="16" width="16" /><img src="q5.gif" height="16" width="16" /><img src="q6.gif" height="16" width="16" /><img src="q7.gif" height="16" width="16" /><img src="q8.gif" height="16" width="16" /><img src="q9.gif" height="16" width="16" /><img src="q10.gif" height="16" width="16" />
ok. 30% zwiększenie szybkości wczytywania
Podział plików na kilka domen
<img src="q1.gif" height="16" width="16" /><img src="q2.gif" height="16" width="16" /><img src="q3.gif" height="16" width="16" /><img src="q4.gif" height="16" width="16" /><img src="q5.gif" height="16" width="16" /><img src="http://mojadomena.net/samples/ch02/q6.gif" height="16" width="16" /><img src="http://mojadomena.net/samples/ch02/q7.gif" height="16" width="16" /><img src="http://mojadomena.net/samples/ch02/q8.gif" height="16" width="16" /><img src="http://mojadomena.net/samples/ch02/q9.gif" height="16" width="16" /><img src="http://mojadomena.net/samples/ch02/q10.gif" height="16" width="16" />
Jeśli kilka aliasów – mechanizm generowania powtarzalnych url Plik grafika.jpg zawsze z s1.domena.com, grafika3.jpg z
s2.domena.com, itp. Wewnątrz własnej kontrolki Control adapter dla Image
(dalej)
ASP.NET i podział na kilka domen
private string _src; private static string[] subdomains = { "http://s1.12titans.net", "http://s2.12titans.net", "http://s3.12titans.net" };
public string src { get { HttpContext ctx = HttpContext.Current; if (ctx.Request.Url.Host != "localhost") { if (!String.IsNullOrEmpty(this._src) && !this._src.StartsWith("http") && !this._src.StartsWith("data:")) { int n = Math.Abs(this._src.GetHashCode()) % subdomains.Length; return subdomains[n] + this._src; } } return this._src; } set { this._src = ResolveUrl(value).ToLowerInvariant(); } }
Skrypty inline mogą opóźniać renderowanie strony Renderowanie dopiero po zakończeniu działania skryptów
OnLoad / DOMReady – wtedy po wyrenderowaniu Umieszczać na końcu pliku Jeśli skrypty zmieniają HTML
Zamiast document.write() – innerHTML (możliwe wywołania później) Ukryty div z document.write() + odkrywanie go później
<script defer> i <script async> (HTML 5) Nie wstrzymuje parsera, pobiera skrypt i wykonuje kod (np. podpina do onload) Async – nie gwarantuje kolejności (wywołuje po pobraniu) Defer – gwarantuje kolejność wywołań
Dołączać z CDN (m.in. ASP.NET AJAX, jQuery) - http://www.asp.net/ajaxlibrary/cdn.ashx
Skrypty
Tylko lower case w miarę możliwości – kompresja <img> zamiast <IMG>, itp.
Image sprites dla wielu mniejszych grafik background-position: 0 -120px
Grafika - rozważyć data URI scheme (IE 8+) Narzędzia lub online – np. dataurl.net Do niewielkich grafik (base64, więc 40% większe) Zwłaszcza w CSS, kiedy może być dodatkowo cache’owane
Zmniejszanie liczby żądań
#hdr{border:1px solid #000;height:40px;background:url(data:image/gif;base64,R0lGODlhAQAoANUAAAAAAP///wFUzgNV0ANVzwVX0QZY0gdY0gha0wpb1Qxd1g1e1w9g2BFh2RJj2xVk3BZm3Rho3hpp4Bxr4h5t4x9u5CFw5SRx5yVz6Cd16Sl26it47C157S987jF97zOA8jJ+8TWB8zaD9DiE9TqF9zqG9zyH+D2I+D6J+T+K+v///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAACoALAAAAAABACgAAAYlwBTqZCqRRqLQB+TpcDaaDOZiqVAmkgjk4WgwFooE4mAoDAiCIAA7)
Javascript Walidacja po stronie klienta Wyłączanie submit po kliknięciu Generowanie długich list (np. select i wiele elementów <option>) Unikać obiekt.inny.jeszczeinny.zmienna (pomocnicza zmienna) Wielokrotny document.write zamiast sklejania stringów textContent szybszy od innerHTML (jeśli element zawiera tekst)
ASP.NET 4.5 – unobtrusive validation
Zmniejszenie liczby żądań – c.d.
<code><add key="ValidationSettings:UnobtrusiveValidationMode" value="WebForms"/></code>
ValidationSettings.UnobtrusiveValidationMode = UnobtrusiveValidationMode.WebForms;
Favicon.ico – lepiej żeby był (cache vs 404 za każdym razem) CSS – inline przy pierwszym wyświetleniu
Mniej żądań przy pierwszym wejściu Skrypt ładowany inline – jeśli nie ustawione ciasteczko W Onload – pobierany dynamicznie (cache dla kolejnych żądań) Nie zamiast – plik potrzebny (cache)
Zmniejszenie liczby żądań – c.d.
<script type="text/javascript"> function getcss() { var h = document.getElementsByTagName('head'); var l = document.createElement('link'); l.type = 'text/css'; l.rel = 'stylesheet'; l.href = 'css/file19.css'; h[0].appendChild(l); }</script>
<system.webServer> <httpProtocol> <customHeaders> <add name="Set-Cookie" value="C=A;expires=Sat, 01-Jan-2050 00:00:00 GMT;path=/demo/css/" /> </customHeaders> </httpProtocol></system.webServer>
Określić <!DOCTYPE> Parser lookahead nie musi restartować
Width i Height przy grafikach Rozmiary kolumn – tabele Charset (dla statycznych stron)
Szybkość renderowania strony
Kiedy wiemy jaka będzie kolejna strona (np. „wizard”) W pageLoad (nie document ready)
Precaching - grafiki
<script type="text/javascript" src="jquery-1.7.1.min.js"></script><script type="text/javascript"> $(window).load(function() { var pre = new Image(0,0); pre.src = "http://s1.domena.net/static/next1.jpg"; var prx = new Image(0, 0); prx.src = "http://s2.domena.net/static/next2.jpg"; });</script>
<img>- dla innych formatów nie zadziała (MIME type) AJAX – można, ale tylko ta sama domena Dynamicznie element <script>
Nie musi być dodawany do DOM Uwaga – parsowane i wywoływane
Dynamicznie <link> Musi być w DOM Wczytywany (może być konflikt selektorów)
Precaching – CSS, JS
<script type="text/javascript"> function preload() { var req = getreq(); if (req != null) { req.open("GET", "/static/next.js", true); req.send(null); } var rex = getreq(); if (rex != null) { rex.open("GET", "/static/next.css", true); rex.send(null); } }</script>
<script type="text/javascript"> function preload() { var scr = document.createElement("script"); scr.src = "http://s1.domena.net/ch02/next.js"; }</script>
<script type="text/javascript"> function preload() { var lnk = document.createElement("link"); lnk.rel = "stylesheet"; lnk.type = "text/css"; lnk.href = "http://s1.domena.net/next.css"; document.getElementsByTagName('head')[0].appendChild(lnk); }</script>
Caching
Każdy ma nieco inną rolę (np. ASP.NET vs http.sys vs SQL)
Wiele rodzajów cache
Cache-Control: max-age lub Expires (dawniej) Jeśli nie ustawione oblicza Aktualny czas +10% różnicy między Last-Modified a aktualnym
Po tym czasie – nadal na dysku, ale nie używane Conditional Get (If-Modified-Since) HTTP 304 – not modified
Statyczne pliki
GET /check.png HTTP/1.1Accept: */*Accept-Language: en-usAccept-Encoding: gzip, deflateIf-Modified-Since: Sat, 10 Jan 2012 10:52:45 GMTIf-None-Match: "80fc52fa8bb2c81:0"User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)Host: www.domena.netConnection: Keep-Alive
<system.webServer> . . . <staticContent> <clientCache cacheControlMode="UseMaxAge" cacheControlMaxAge="365.00:00:00" /> </staticContent></system.webServer>
Cache-Control: no-cache Przeglądarka musi sprawdzić, ale może korzystać z cache
(back/fwd) Cache-Control: no-store
Całkowite wyłączenie (także back/forward)
Cache statyczny - wyłączanie
<location path="image.jpg"> <system.webServer> <staticContent> <clientCache cacheControlMode="DisableCache" /> </staticContent> </system.webServer>
Output cache VaryByParam, VaryByHeader – np. Accept-Language (dla różnych
języków) VaryByControl (np. „src”, jeśli taką właściwość ma nasza User
Control) Dla User Controls – ustawiać to samo ID na różnych stronach
i Shared=true Jeśli shared=false to do porównywania ID brana także nazwa Page
Dynamiczna zawartość
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="dyn-client.aspx.cs" Inherits="dyn_client" %><%@ OutputCache Duration="86400" Location="Client" VaryByParam="None" %>
<system.web> <caching> <outputCacheSettings> <outputCacheProfiles> <add name="Cache1Day" duration="86400" location="Client" varyByParam="none" /> </outputCacheProfiles> </outputCacheSettings> </caching></system.web>
<%@ OutputCache CacheProfile="Cache1Day" %>
Najprościej – VaryByCustom = „browser” Po swojemu - global.asax
Jeśli chcemy np. jedną wersję dla wszystkich IE i jedną dla Webkit
Cache dla różnych przeglądarek
public override string GetVaryByCustomString(HttpContext context, string custom) { switch (custom.ToLower()) { case "iemozilla": switch (context.Request.Browser.Browser.ToLower()) { case "ie": case "blazer 3.0": return "ie"; case "mozilla": case "firebird": case "firefox": case "applemac-safari": return "mozilla"; default: return "default"; } default: return base.GetVaryByCustomString(context, custom); } }
Dynamiczna zawartość - wyłączanie
this.Response.Cache.SetCacheability(HttpCacheability.NoCache); this.Response.Cache.SetAllowResponseInBrowserHistory(true); // Aby nie było Expires (niepotrzebne)
this.Response.AppendHeader("Cache-Control", "no-store"); // Dla no-store
<%@ OutputCache Location=„None" %>
Cache-Control: no-cachePragma: no-cacheExpires: -1
Czasem chcemy mieć pewność, że dane aktualne Aspx, kod lub profil w web.config
Domyślnie ASP.NET ustawia Cache-Control: private Nie będzie cache’owane przez proxy Możliwe nadpisanie (Cache-Control: public)
<%@ OutputCache Location=„Any” %> Location Downstream – tylko w przeglądarkach
Dynamiczna zawartość – c.d.
this.Response.Cache.SetMaxAge(age); this.Response.Cache.SetExpires(DateTime.UtcNow + age); this.Response.Cache.SetLastModified(DateTime.UtcNow); this.Response.Cache.SetCacheability(HttpCacheability.Public); this.Response.Cache.SetNoServerCaching();
Rozszerzalność
<caching> <outputCache defaultProvider="AspNetInternalProvider"> <providers> <add name="DiskCache" type="Test.OutputCacheEx.DiskOutputCacheProvider, DiskCacheProvider"/> </providers> </outputCache></caching>
ASP.NET 4.0+ – możliwe np. zapisywanie na dysku
Możliwa dynamiczna podmiana Np. top 10 w pamięci, reszta na dysku
Stan tymczasowy strony, stan kontrolek serwerowych Często lepsze od sesji (baza przy farmie)
LosFormatter – szybki dla typów string, hashtable, arraylist, pair, triple, int, boolean Dla pozostałych – BinaryFormatter (wolny), ale można napisać własny TypeConverter Lepiej – kolekcja prostych typów niż własny obiekt (bez TypeConvertera)
Obecność elementu wykorzystywana m.in. do rozpoznania Page.IsPostback
ViewStateMac – zabezpieczenie przed modyfikacjami Zabezpieczenie przez CSRF
W Page_init – ViewState[userkey] = this.User.Identy.Name;
ViewState
Ma tendencje do rozrastania się Może być prościej wysłać zapytanie niż uploadować dużo większy formularz
Możliwość wyłączenia Od ASP.NET 4 – nie tylko dla całej strony (EnableViewState), ale pojedynczych
kontrolek ViewStateMode – enabled, disabled, inherit (domyślnie) Warto domyślnie wyłączyć (w web.config) i włączać tam, gdzie potrzeba
ControlState – część, której nie można wyłączyć
ViewState - problemy
Tag Transform, aby wyłączyć Control State Jeśli nie korzystamy z zaawansowanych funkcji kontrolki
ViewState – wyłączanie
public class ListViewNoCS : ListView { protected override object SaveControlState() { return null; } }
<pages> . . . <tagMapping> <add tagType="System.Web.UI.WebControls.ListView" mappedTagType="Samples.ListViewNoCS" /> </tagMapping></pages>
Dla wolnych połączeń (np. klienci mobile) można przechowywać po stronie serwera
Można uzależnić to od typu połączenia lub rozmiaru ViewState
ViewState – wolne połączenia
protected override void SavePageStateToPersistenceMedium(object state) { string key = Guid.NewGuid().ToString(); this.ClientScript.RegisterHiddenField(ViewKeyName, key); this.Cache[key] = state; }
protected override object LoadPageStateFromPersistenceMedium() { string key = this.Request[ViewKeyName]; if (key == null) throw new InvalidOperationException("Invalid ViewState Key"); object state = this.Cache[key]; if (state == null) throw new InvalidOperationException("ViewState too old"); return state; }
Dla domeny - max 50, 10 KB / cookie Session lub persistent Domyślna wartość path - „/”
Dołączane także do zawartości statycznej wszystkich plików! Ustawiać zawsze cookie.Path = „/katalog/”
Bez ustawienia Domain IE – także wszystkie subdomeny Inne przeglądarki – tylko aktualna domena Lepiej ustawić cookie.Domain (unikanie błędów)
Cookie.HttpOnly = true Cookie niewidoczne dla JS (mniej możliwości ataku) Powinno się ustawiać domyślnie dla wszystkich
Cookie.Secure = true – wysyłane tylko przez SSL Jeśli nie możemy SSL – konwersja do Base64 (Convert.ToBase64String) i szyfrowanie
symetryczne
Cookies
IE8+, Firefox 3.5+, Safari 4+, Chrome 4+, Opera 10.5+ 10 MB – IE, 5 MB - pozostałe
2 typy Per-session (do zamkniecia okna) Per-domain
Tylko string, ale można serializować do JSON Tylko klient – nie przesyłane do serwera
Można dodać np. jako parametr wywołania usługi WCF
HTML 5 Web Storage
<script type="text/javascript">
sessionStorage.setItem('mykey', 'this is my value'); var item = sessionStorage.getItem('mykey')
</script>
Kernel-mode driver - http.sys Znacznie szybszy (bez context switching, itd.) Domyślnie włączony dla zawartości statycznej
Dla dynamicznej – OutputCache, ustawienie IIS lub applicationHost.config
Kiedy nie działa (najczęstsze) Zawartość z querystring Domyślny dokument (d.com zamiast d.com/default.htm) Włączona kompresja
Domyślnie sliding, 120 sekund HKLM\System\CurrentControlSet\Services\Http\Parameters\UriScavengerPeriod Warto zmienić w razie potrzeby (np. kilkanaście godzin) - ostrożnie
Windows Kernel Cache
Domyślnie włączony – jeśli ustawione OutputCache Także dla żądań z querystring
Dla własnych HttpHandlerów – włączyć User Mode Caching
http://d.com/katalog/ zamiast http://d.com/katalog W przeciwnym wypadku HTTP 302 Zapobiega cache’owaniu 3 różnych wersji
IIS Cache
Podobnie jak IIS, ale VaryByParam może być custom Np. kontekst użytkownika
Fragment Cache – OutputCache w User Control Substitution Cache
Cała strona cache’owana, oprócz <asp:Substitution>
Cache ASP.NET
<body>Cached time: <%= DateTime.Now.ToString() %><br />Page time:<asp:Substitution ID="sub" runat="server" MethodName="SubTime" /></body>
public static string SubTime(HttpContext context) { return DateTime.Now.ToString(); }
Plik Baza danych
Konieczne włączenie Service Broker
Cache Dependency
CacheDependency depend = new CacheDependency(this.MapPath("~/depend.txt")); this.CachePolicy.Dependency = depend;
using (SqlConnection conn = new SqlConnection(cs)) { string sql = "dbo.GetInfo"; using (SqlCommand cmd = new SqlCommand(sql, conn)) { cmd.CommandType = CommandType.StoredProcedure; conn.Open(); SqlCacheDependency dep = new SqlCacheDependency(cmd); mygrid.DataSource = cmd.ExecuteReader(); mygrid.DataBind(); this.Response.AddCacheDependency(dep); } }
// SqlDependency.Start() w web.config
Np. AppFabric
Cache Provider
public class MemoryCacheProvider : OutputCacheProvider { public override object Add(string key, object entry, DateTime utcExpiry) { object result = HttpRuntime.Cache[key]; if (result == null) { this.Set(key, entry, utcExpiry); result = entry; } return result; }
public override object Get(string key) { return HttpRuntime.Cache[key]; }
public override void Remove(string key) { HttpRuntime.Cache.Remove(key); }
public override void Set(string key, object entry, DateTime utcExpiry) { HttpRuntime.Cache.Insert(key, entry, null, utcExpiry, Cache.NoSlidingExpiration, CacheItemPriority.High, null); } }
<system.web> <caching> <outputCache> <providers> <add name="MemoryCacheProvider" type="Samples.MemoryCacheProvider" /> </providers> </outputCache> </caching> </system.web>
Kiedy chcemy jak najwięcej, jak najdłużej (na ile pamięć pozwoli)
Omijamy logikę Cache Policy - szybkość Nie dla web farm WeakReference i GC
Statyczne pola – uproszczony cache
public static class Weak{ public static WeakReference MyItem { get; set; } public static readonly Object lockObject = new Object();
public static DataSet WeakData() { DataSet ds = null; lock (lockObject) { if (MyItem != null) ds = MyItem.Target as DataSet; if (ds == null) { ds = new DataSet(); MyItem = new WeakReference(ds); } } return ds; }}
IIS
Domyślnie – jeden proces w3wp.exe AppPool - kilka procesów dla kolekcji aplikacji
(Web Garden) Maximum Worker Processes
AppPool Recycling Tracimy cache, inproc session, static, … Domyślnie 29h Warto zmienić na określoną godzinę lub
liczbę obsłużonych requestów (NLB)
App Pool
Wiele AppPools Dzielenie aplikacji na kilka puli Błąd w jednej puli nie ma wpływu na działanie drugiej Np. obsługa magazynu i aplikacja dla konsumentów
Web Garden Ochrona przed błędami procesu w3wp – kilka procesów Uwaga - context-switching między wątkami szybszy niż między
procesami! Konieczna duplikacja w pamięci danych takich jak cache czy pola
static Zalecane max 1-2 worker procesy / CPU core Tylko, kiedy najistotniejsza ciągłość pracy
Kiedy najważniejsze jest działanie
ASP.NET – ISAPI Extension Duplikacja funkcjonalności
2 odrębne pipeline’y Np. Logowanie, auth -> IIS, później ASP.NET Trudniejsza konfiguracja
IIS6 + ASP.NET (Classic mode)
Authentication
Basic NTLM Anon
...
DetermineHandler
...
SendResponse
HTTP Request
HTTP Response
CGI
Static File
ISAPI
Compression
Log
aspnet_isapi.dll
Authentication
MapHandler
Forms Windows
...ASPX
Trace
...
...
Wyodrębnione komponenty z w3core.dll Niezależne dodawanie / usuwanie
funkcjonalności Np. auth – możemy dać własne zamiast
basic / LDAP, itp.. Lekki core
Architektura IIS 7
Authentication
...
ExecuteHandler
...
SendResponse
HTTP Request
HTTP Response
Authorization
UpdateCache
ResolveCache
Authentication
...
DetermineHandler
...
SendResponse
HTTP Request
HTTP Response
BasicNTLM Anon
CGI
Static File
ISAPI
Log Compression
UrlAuthz
OutputCache
Forwarder
Basic40+
Handler HTTP Mapowany dla rozszerzeń (np. aspx) Migracja z html do dynamic ->
można dodać mapowanie htm naASP.NET
Moduły HTTP ASP.NET bezpośrednio w IIS Pipeline Np. sesja, uwierzytelnianie
dla zawartości statycznej, php, itp. Rozszerzalność przez .NET,
a nie ISAPI (C++) Usuwać niepotrzebne moduły Kolejność typów dokumentów w IIS
IIS 7 + ASP.NET - Integrated Pipeline
ISAPI
Authentication
...
ExecuteHandler
...
SendResponse
Authorization
UpdateCache
ResolveCache
HTTP Request
HTTP Response
Anon
aspnet_isapi.dll
Authentication
MapHandler
...
...
Forms Windows
ASPX
Trace
...
Basic
Compression
Log
Static File
Nowe w Windows Server 2008 Rezerwacja minimalnej ilości pamięci lub CPU dla
procesów
Windows System Resource Manager
Bezpieczeństwo i mniej pobieranych danych X-Powered-By – IIS Nagłówek Server – tylko z kodu Etag – przy web farm można wyłączyć (często jest różny dla node’ów) X-Aspnet-Version - <httpRuntime enableVersionHeader="false"/>
Usuwanie nagłówków
public class HttpHeaderCleanup : IHttpModule { public void Init(HttpApplication context) { context.PreSendRequestHeaders += OnPreSendRequestHeaders; }
void OnPreSendRequestHeaders(object sender, EventArgs e) { HttpResponse response = HttpContext.Current.Response; response.Headers.Remove("Server"); response.Headers.Remove("ETag"); }
public void Dispose() { } }
3-5% więcej CPU, ale generalnie warto Gzip (domyślny) lub deflate (mniejszy nagłówek)
Kompresja
<httpCompression directory="%SystemDrive%\inetpub\temp\IIS Temporary Compressed Files" dynamicCompressionDisableCpuUsage="100"> <scheme name="gzip" dll="%Windir%\system32\inetsrv\gzip.dll" staticCompressionLevel="9" dynamicCompressionLevel="5" /> <scheme name="deflate" dll="%Windir%\system32\inetsrv\gzip.dll" staticCompressionLevel="9" dynamicCompressionLevel="5" /> <staticTypes> <add mimeType="text/*" enabled="true" /> <add mimeType="message/*" enabled="true" /> <add mimeType="application/x-javascript" enabled="true" /> <add mimeType="*/*" enabled="false" /> </staticTypes> <dynamicTypes> <add mimeType="text/*" enabled="true" /> <add mimeType="message/*" enabled="true" /> <add mimeType="application/x-javascript" enabled="true" /> <add mimeType="*/*" enabled="false" /> </dynamicTypes></httpCompression>
Lepsze dla SEO http.sys nawet przy querystringach Krótsze URLe w odpowiedziach Ukrywa technologię aplikacyjną IIS (URL Rewrite) lub ASP.NET
URL Rewriting
Pamiętać o botach (robots.txt, sitemap.xml)
Bandwidth Throttling Moduł BitRateThrottling do IIS Głównie dla mediów, ale również
można do innych typów danych Np. wolniej dla botów lub kawałek
dużego zdjęcia szybko
Szybkość transferu public class Throttle : IHttpModule { public void Init(HttpApplication context) { context.PostRequestHandlerExecute += OnPostRequestHandlerExecute; }
void OnPostRequestHandlerExecute(object source, EventArgs e) { HttpApplication application = (HttpApplication)source; HttpContext context = application.Context; HttpResponse response = context.Response; if (response.ContentType == "application/x-zip-compressed") { HttpRequest request = context.Request; if (!String.IsNullOrEmpty(request.ServerVariables["SERVER_SOFTWARE"])) { request.ServerVariables["ResponseThrottler-InitialSendSize"] = "20"; request.ServerVariables["ResponseThrottler-Rate"] = "10"; } } }
public void Dispose() { } }
ASP.NET
Tradycyjne przetwarzanie żądań“thread-per-request” a.k.a. “post office”
Thread pool
Żądania
BusyBusy Busy Busy
Przy load testach – zużycie CPU niewielkie, ale długi czas odpowiedzi
Przetwarzanie asynchronicznea.k.a. “restaurant”
Żądania
Thread pool
Domyślnie 12 / CPU Czasem zwiększenie liczby pomaga
Wiąże się z tym koszt (start, pamięć, context switch) Ważniejsza optymalizacja istniejących
Blokujące wywołania – baza, zewnętrzne usługi, I/O Przy obliczeniach (CPU) nie pomoże!
Worker Threads
Domyślnie przetwarzanie synchroniczne (1 wątek całe lifecycle)
Async point
Synchronicznie i asynchronicznie
Asynchronicznie – web forms (APM)<%@ Page Async="true„ AsyncTimeout=„30” Language="C#" AutoEventWireup="true" CodeFile="sql-async.aspx.cs" Inherits="sql_async" %>
public const string ConnString = "Data Source=.;Integrated Security=True;Async=True";
protected void Page_Load(object sender, EventArgs e) { PageAsyncTask pat = new PageAsyncTask(BeginAsync, EndAsync, null, null, true); this.RegisterAsyncTask(pat); }
private IAsyncResult BeginAsync(object sender, EventArgs e, AsyncCallback cb, object state) { SqlConnection conn = new SqlConnection(ConnString); conn.Open(); SqlCommand cmd = new SqlCommand("WAITFOR DELAY '00:00:01'", conn); IAsyncResult ar = cmd.BeginExecuteNonQuery(cb, cmd); return ar; }
private void EndAsync(IAsyncResult ar) { using (SqlCommand cmd = (SqlCommand)ar.AsyncState) { using (cmd.Connection) { int rows = cmd.EndExecuteNonQuery(ar); } } }
Task – zaczyna od razu APM – kolejkuje do async point
Asynchronicznie – web forms (Task)
public const string ConnString = "Data Source=.;Integrated Security=True;Async=True";
protected async void Page_PreRender(object sender, EventArgs e) { using (SqlConnection conn = new SqlConnection(ConnString)) { conn.Open(); using (SqlCommand cmd = new SqlCommand("WAITFOR DELAY '00:00:01'", conn)) { await Task.Factory.FromAsync<int>(cmd.BeginExecuteNonQuery, cmd.EndExecuteNonQuery, null); } } }
Najlepiej agregować (np. kilka zapytań w procedurze skł.)
Wywoływane równolegle lub jedno po drugim
Asynchroniczne strony – wiele zadań
protected void Page_Load(object sender, EventArgs e) { // Dwa pierwsze – równolegle. Następnie BeginAsync3 (ostatni parametr PageAsyncTask) PageAsyncTask pat = new PageAsyncTask(BeginAsync1, EndAsync1, null, null, true); this.RegisterAsyncTask(pat); pat = new PageAsyncTask(BeginAsync2, EndAsync2, null, null, true); this.RegisterAsyncTask(pat); pat = new PageAsyncTask(BeginAsync3, EndAsync3, null, null, false); this.RegisterAsyncTask(pat); }
Czasem potrzeba zarejestrowania zadania po zakończeniu poprzedniego Np. drugie wywołanie usługi po zakończeniu poprzedniego Konieczne wywołanie ExecuteRegeisteredAsyncTasks() Można także wywołać, aby wymusić wywołanie przed async point
Dla Task – zwyczajnie jeden po drugim z await
Późniejsza rejestracja
private void EndAsync(IAsyncResult ar) { using (SqlCommand cmd = (SqlCommand)ar.AsyncState) { using (cmd.Connection) { int rows = cmd.EndExecuteNonQuery(ar); } } PageAsyncTask pat = new PageAsyncTask(BeginAsync2, EndAsync2, null, null, true); this.RegisterAsyncTask(pat); this.ExecuteRegisteredAsyncTasks(); }
Usługi Pliki
Inne operacje
protected async void Page_Load(object sender, EventArgs e) { var terra = new TerraServiceSoapClient(); Place place = new Place() { City = "Seattle", State = "WA", Country = "US" }; var result = await terra.GetPlaceFactsAsync(place); PlaceFacts facts = result.Body.GetPlaceFactsResult; this.LA.Text = String.Format("Latitude: {0:0.##}", facts.Center.Lat); this.LO.Text = String.Format("Longitude: {0:0.##}", facts.Center.Lon); }
// Dla web requestów: // var r = WebRequest.Create(„http://…”); // var res = await r.GetResponseAsync();
private IAsyncResult BeginAsync(object s, EventArgs e, AsyncCallback cb, object s) { FileStream fs = new FileStream(this.Server.MapPath("csg.png"), FileMode.Open, FileAccess.Read, FileShare.Read, 4096, FileOptions.Asynchronous | FileOptions.SequentialScan); this.Data = new byte[64 * 1024]; IAsyncResult ar = fs.BeginRead(this.Data, 0, this.Data.Length, cb, fs); return ar;
// dla podejścia Task: // int size = await fs.ReadAsync(this.Data, 0, this.Data.Length);
}
Async controller – przed MVC 4 public class WeatherAsyncController : AsyncController { private string _forecastUrl = "http://weather.yahooapis.com/forecastjson?w=523920&u=c";
public void IndexAsync() { var webClient = new WebClient();
AsyncManager.OutstandingOperations.Increment();
webClient.DownloadStringCompleted += (sender, evt) => { // capture result when web service completes AsyncManager.Parameters["json"] = evt.Result; AsyncManager.OutstandingOperations.Decrement();
// Exception handling - challenge... };
// async call webClient.DownloadStringAsync(new Uri(_forecastUrl)); }
public ActionResult IndexCompleted(string json) { WeatherData weather = new JavaScriptSerializer().Deserialize<WeatherData>(json); return View("Weather", weather); } }
Async controller – MVC 4
public class WeatherTaskAsyncController : AsyncController { private string _forecastUrl = "http://weather.yahooapis.com/forecastjson?w=523920&u=c";
public async Task<ActionResult> Index() { string json = await new WebClient().DownloadStringTaskAsync(_forecastUrl); WeatherData weather = new JavaScriptSerializer().Deserialize<WeatherData>(json); return View("Weather", weather); } }
Możliwość wywołania kodu równolegle Po wyrenderowaniu strony lub w trakcie
ThreadPool.QueueUserWorkItem() Zużywa wątki z AppPool
1 wątek w tle + kolejka Typowy consumer - producer Np. logowanie (nie wymagane 100% działanie) Bardziej krytyczne zadania – np. Service Broker lub Azure Worker + kolejka
Przy wielu wątkach – warto ReaderWriterLockSlim EnterReadLock / EnterWriteLock (mniejsze ryzyko zakleszczenia)
Przykład
Background Worker Thread
InProc Najszybsza, ale load balancer tylko sticky Nie odporna na awarie sprzętu
StateServer Web farm – ok, ale single point of failure
SQL (+SQL Agent do usuwania) Możliwe przyspieszenie przez <sessionState compressionEnabled=„true” /> <%@ Page EnableSessionState=„false” @> - ale i tak update (timeout) <%@ Page EnablesessionState=„ReadOnly” @> - mniej locków; ta sama SP, która odczytuje
Przyspieszyć zapis do logu (podział na dyski, SSD, itp.) PartitionResolver (wybór conn str na podst id sesji) i SessionIDManager (id sesji z
nr maszyny) Mimo wszystko niezbyt skalowalne – patrz: AppFabric session provider
Sesja
Control ID
Możliwość kontroli identyfikatorów (klienckich) kontrolek serwerowych
Control.ClientIdMode Legacy Static Predictable Inherit (domyślne dla kontrolek)
Kolekcje – ClientIDRowSuffix Dla całej strony lub kontrolki Ostrożnie – duplikacja, długość
Możliwość zmiany markupu kontrolki serwerowej Np. GridView nie z tabelkami, ale na floatach
Control Adapter
// Zamiana URL w Image na małe litery
public class ImageControlAdapter : WebControlAdapter { public ImageControlAdapter() { }
protected override void BeginRender(System.Web.UI.HtmlTextWriter writer) { Image image = Control as Image; if ((image != null) && !String.IsNullOrEmpty(image.ImageUrl)) { if (!image.ImageUrl.StartsWith("http") && !image.ImageUrl.StartsWith("data:")) { image.ImageUrl = this.Page.ResolveUrl(image.ImageUrl).ToLower(); } } base.BeginRender(writer); } }
// adapter.browser
<browsers> <browser refID="Default"> <controlAdapters> <adapter controlType="System.Web.UI.WebControls.Image" adapterType="Samples.ImageControlAdapter" /> <adapter controlType="System.Web.UI.WebControls.Panel" adapterType="Samples.NoIdControlAdapter" /> <adapter controlType="System.Web.UI.WebControls.Label" adapterType="Samples.NoIdControlAdapter" /> <adapter controlType="System.Web.UI.WebControls.HyperLink" adapterType="Samples.NoIdControlAdapter" /> </controlAdapters> </browser></browsers>
Np. wyłączenie domyślnego generowania ID
Control Adapter c.d. – usuwanie ID
public class NoIdControlAdapter : WebControlAdapter { protected override void Render(HtmlTextWriter writer) { PageBase page = this.Page as PageBase; if ((page != null) && page.RemoveIds && (this.Control.ClientIDMode != ClientIDMode.Static)) { HtmlTextWriter noIdwriter = new NoIdHtmlWriter(writer); base.RenderBeginTag(noIdwriter); base.RenderContents(writer); base.RenderEndTag(noIdwriter); } else { base.Render(writer); } } }
public class NoIdHtmlWriter : HtmlTextWriter { public NoIdHtmlWriter(TextWriter writer) : base(writer) { }
public override void AddAttribute(HtmlTextWriterAttribute key, string value) { if (key != HtmlTextWriterAttribute.Id) base.AddAttribute(key, value); } }
public class PageBase : Page{ protected override void OnInit(EventArgs e) { base.OnInit(e); this.RemoveIds = true; }
public bool RemoveIds { get; set; }}
Np. do zwracania zapisanego w bazie HTML Bez cyklu życia ASP.NET – szybciej Asynchronicznie – uczestniczy w każdym żądaniu! IsReusable – czy jedna instancja może być dla wielu żądań
HTTP Handler w .NET
<%@ WebHandler Language="C#" Class="Handler" %>
using System;using System.Data;using System.Web;using System.Data.SqlClient;
public class Handler : IHttpAsyncHandler { public const string ConnString = "Data Source=.;Initial Catalog=Sample;Integrated Security=True;Async=True"; HttpContext Context { get; set; }
public void ProcessRequest(HttpContext context) { }
public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData) { this.Context = context; int fileid = 0; string id = context.Request.QueryString["id"]; if (!String.IsNullOrEmpty(id)) fileid = Convert.ToInt32(id); SqlConnection conn = new SqlConnection(ConnString); conn.Open(); SqlCommand cmd = new SqlCommand("GetHtml", conn); cmd.CommandType = CommandType.StoredProcedure; cmd.Parameters.Add("fileId", SqlDbType.Int).Value = fileid; IAsyncResult ar = cmd.BeginExecuteReader(cb, cmd); return ar; }
public void EndProcessRequest(IAsyncResult ar) { using (SqlCommand cmd = (SqlCommand)ar.AsyncState) { using (cmd.Connection) { SqlDataReader reader = cmd.EndExecuteReader(ar); while (reader.Read()) { Context.Response.Write(reader["Html"]); } } } Context.Response.ContentType = "text/html"; } public bool IsReusable { get { return false; } }}
Krótsze adresy w kodzie http.sys caching Łączenie np. z ASP.NET MVC (MapRoute /
MapPageRoute)
Routing
protected void Application_Start(object sender, EventArgs e) { RouteTable.Routes.Add("Category", new Route("{category}", new PageRouteHandler("~/1.3 Routing.aspx"))); RouteTable.Routes.Add("CategoryAndPage", new Route("{category}/{page}", new PageRouteHandler("~/1.3 Routing.aspx"))); }
protected void Page_Load(object sender, EventArgs e) { lblCategory.Text = RouteData.Values["category"] as string; lblPage.Text = RouteData.Values["page"] as string; }
Klasyczne this.Response.Redirect("~/pages/error.aspx", true);
True – czy zakończyć request this.Response.RedirectPermanent this.Response.RedirectToRoute
Server.Transfer(„~/pages/error.aspx”, false) False – czy przekazać parametry z obecnej strony True – dla ViewState wyrzuci exception; tylko dla
querystring Cross-page postback
Przekierowania
<asp:Button runat="server" PostBackUrl="~/pages/otherpage.aspx" Text="Submit" />
Zawartość z Render() buforowana i zwracana na koniec Dla długotrwałych zadań może wymagać czasu
Najlepiej – z Ajax Response.Flush() – wysłanie odpowiedzi z bufora
Przed Render() nic w nim nie ma!
Wczesny flush odpowiedzi
W OnPreRender Ręczny zapis odpowiedzi Wyrenderowanie wybranych kontrolek serwerowych Usunięcie wyrenderowanych kontrolek (aby nie było duplikacji po
Render) Długotrwałe zadanie w OnPreRender (async) lub
synchronicznie po PreRender (po async point)
Wczesny flush – c.d.
<html><head id="Head1" runat="server"> <title></title> <script type="text/javascript" src="test.js"></script></head><body> <form id="form1" runat="server"> <div> <asp:Label runat="server" ID="test" Text="testing" /> </div> </form></body></html>
protected override void OnPreRender(EventArgs e) { base.OnPreRender(e); this.Response.Write("<!DOCTYPE html PUBLIC " + "\"-//W3C//DTD XHTML 1.0 Transitional//EN\" " + "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n"); this.Response.Write("<html xmlns=\"http://www.w3.org/1999/xhtml\">\n"); HtmlTextWriter writer = this.CreateHtmlTextWriter(this.Response.Output); this.Header.RenderControl(writer); writer.Flush(); this.Response.Flush(); this.Controls.Remove(this.Header); Thread.Sleep(2000); }
Wczesny flush - odpowiedźHTTP/1.1 200 OKCache-Control: privateTransfer-Encoding: chunkedContent-Type: text/html; charset=utf-8Server: Microsoft-IIS/7.5X-AspNet-Version: 4.0.30319X-Powered-By: ASP.NETDate: Fri, 03 Feb 2012 12:16:11 GMT161<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN""http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head id="Head1"><link href="App_Themes/mkt/common.css" type="text/css" rel="stylesheet" /><title>Testing</title><script type="text/javascript" src="test.js"></script></head>146<body><form name="form1" method="post" action="flush1.aspx" id="form1"><div><input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE"value="/wEPDwULLTE0NDMxNDM0MTlkZA0loE+taD1AMKhxDNDZZLZADwxZqGnPPbIF8Mylq4PV" /></div><div><span id="test">testing</span></div></form></body></html>0
Minification dla dynamicznych public class MinifyStream : Stream { private StreamWriter Writer { get; set; } private Decoder Utf8Decoder { get; set; } public MinifyStream(Stream stream) { this.Writer = new StreamWriter(stream, Encoding.UTF8); this.Utf8Decoder = Encoding.UTF8.GetDecoder(); }
public override void Write(byte[] buffer, int offset, int count) { int characterCount = this.Utf8Decoder.GetCharCount(buffer, offset, count); char[] result = new char[characterCount]; int decodedCount = this.Utf8Decoder.GetChars(buffer, offset, count, result, 0); if (decodedCount <= 0) return; // ... wyfiltrowanie zbędnych znaków i zapis do this.Writer }
public override void Close() { this.Writer.Flush(); this.Writer.Close(); base.Close(); }
public override void Flush() { this.Writer.Flush(); }
// ... }
// W module HttpModule
private void Sample_PostRequestHandlerExecute(Object source, EventArgs e) { HttpApplication application = (HttpApplication)source; HttpResponse response = application.Context.Response; if(response.ContentType == "text/html") response.Filter = new MinifyStream(response.Filter); }
Page.IsPostBack Rozpoznawanie odświeżenia strony
Unikanie niepotrzebnej pracy
protected virtual bool IsRefresh { get { return this.Request.Headers["Pragma"] == "no-cache" || this.Request.Headers["Cache-Control"] == "max-age=0"; } }
Response.IsClientConnected Czy przeglądarka jeszcze czeka na odpowiedź, czy została
zamknięta? Warto sprawdzić przed długotrwałą operacją na bazie
<compilation batch=true /> True – niewiele pakietów False – każda strona w osobnym assembly (wolniej, ale łatwiej
zmiany) Wyłączyć debug mode!
Można wymusić w machine.config
Inne
<system.web> <deployment retail="true" /></system.web>
Auto-start aplikacji
//applicationHost.config
<serviceAutoStartProviders> <add name="PrewarmMyCache" type="MyNamespace.CustomInitialization, MyLibrary" /></serviceAutoStartProviders>
public class CustomInitialization : System.Web.Hosting.IProcessHostPreloadClient{ public void Preload(string[] parameters) { // Nasza inicjalizacja. }}
Dawniej – Global.asax i Application_Load IIS 7.5 i Windows Server 2008 R2
Książka Ultra-Fast ASP.NET 4.5 Książka Ultra-Fast ASP.NET MSDN – ASP.NET Performance
Zasoby
© 2012 Microsoft Corporation. Wszelkie prawa zastrzeżone. Microsoft, Windows oraz inne nazwy produktów są lub mogą być znakami towarowymi lub zastrzeżonymi znakami towarowymi firmy Microsoft w Stanach Zjednoczonych i innych krajach. Zamieszczone informacje mają charakter wyłącznie informacyjny. FIRMA MICROSOFT NIE UDZIELA ŻADNYCH GWARANCJI (WYRAŻONYCH WPROST LUB DOMYŚLNIE), W TYM TAKŻE USTAWOWEJ RĘKOJMI ZA WADY FIZYCZNE I PRAWNE, CO DO INFORMACJI ZAWARTYCH W TEJ PREZENTACJI.