Try Sending Wczoraj kolega zrzucił mi wyzwanie. Spytał mnie czy kiedyś przez WCF wysłałem 600.000 rekordów. Oczywiście tego nie robiłem, ponieważ dlaczego usługa typu REST dla telefonu Android powinna przekazywać 600.000 rekordów.
Wyzwanie jest wyzwaniem. Udowodnię kto z nas jest lepszym programistą raz na zawsze.
Teraz bardziej na serio. Planowałem zrobić wpis na temat usługi sieciowej WCF, która przesyła duże dane. Żadnych magicznych ustawień czy skomplikowanego “stremowania“. Przy użyciu swojej pomysłowości można rzeczywiście rozwiązać ten problem.
Tematyka jednak zajęła mi więcej czasu niż myślałem. Wczoraj próbowałem na siłę sprawdzić ile rekordów jestem wstanie przesłać mają 8 GB Ramu i procesor Intel core i7 2670qm. Pamiętam jak kiedyś przy tworzeniu kolekcji większej niż 200.000 rekordów otrzymywałem wyjątek “Out of Memory Exception” na moim starym laptopie. Pewne granice są też zależne od twojego sprzętu.
Mówiąc krótko to było głupie ponieważ z góry wiedziałem ,że nie uda mi się “normalnie” przesłać 2 milionów rekordów. Z drugiej strony na blogu miałem czasami jeszcze lepsze pomysły na wpis “Niektóre wyjątki, jakie mogą pojawić się w C#”.
Oto pierwsza część cyklu “WCF Big Large Data”. W tej części pokaże, dlaczego nie powinieneś przesyłać dużych informacji w normalny sposób w WCF. W następnej części zastosujemy właściwe techniki przesyłania.
Kod usługi sieciowej
Oto obiekt, który będziemy przesyłać przez WCF-a 2 miliony razy. Aby się upewnić ,że będzie on dość pojemny pamięciowo umieścimy w nim długie napisy.
[DataContract]
public class ImaginaryTableEntry
{
[DataMember]
public string ImaginaryString;
[DataMember]
public string ImaginaryBiggerString;
[DataMember]
public string ImaginaryReturnValue;
[DataMember]
public bool IsChanged = false;
}
Na razie usługa sieciowa będzie tworzyć dużą listę danych a potem będziemy ją przesyłać.
[ServiceContract]
public interface IDataService
{
[OperationContract]
List<ImaginaryTableEntry> GetImaginaryTableEntries();
[OperationContract]
void CreateImaginaryData();
}
Umieszczenie 2 milionów rekordów do bazy danych byłoby kłopotliwe dlatego dane są umieszczane do pliku CSV. Jak widać obiekty “ImaginaryTableEntry” będą zawierać dosyć długie napisy.
public void CreateImaginaryData()
{
string path = System.Configuration.ConfigurationManager.AppSettings["DataPath"];
try
{
StreamWriter sw = new StreamWriter(path);
string padded;
for (int i = 0; i < 2000000; i++)
{
padded = i.ToString(CultureInfo.InvariantCulture).PadLeft(5, '0');
sw.Write("Record number" + padded +
";Big String " + padded + " just some random numbers" + RandomNumberText(false) +
";Big Number " + padded + RandomNumberText(true) + "\r\n");
}
sw.Close();
}
catch (Exception ex)
{
}
}
public string RandomNumberText(bool joined)
{
Random r = new Random(Guid.NewGuid().GetHashCode());
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 6; i++)
{
sb.Append(r.Next(1000000000, int.MaxValue).ToString(CultureInfo.InvariantCulture));
if (!joined)
sb.Append(" ");
}
return sb.ToString();
}
Plik CSV tylko z dwoma milionami rekordów waży 380 MB.
Sprawa zapowiada się ciekawie. Oto kod metody w usłudze sieciowej, która odczyta wcześniej utworzony plik CSV i spróbuje wysłać 2 miliony rekordów do klienta.
public List<ImaginaryTableEntry> GetImaginaryTableEntries()
{
string path = System.Configuration.ConfigurationManager.AppSettings["DataPath"];
List<ImaginaryTableEntry> result = new List<ImaginaryTableEntry>();
try
{
StreamReader sr;
string line;
string[] lineSplit;
sr = File.OpenText(path);
while (!sr.EndOfStream)
{
line = sr.ReadLine();
if (line != null)
{
lineSplit = line.Split(myInnerSeparator, StringSplitOptions.None);
result.Add(new ImaginaryTableEntry()
{
ImaginaryString = lineSplit[0],
ImaginaryBiggerString = lineSplit[1],
ImaginaryReturnValue = lineSplit[2]
});
}
}
sr.Close();
return result;
}
catch (Exception ex)
{
}
return null;
}
Na początku próbowałem przez serwer wcisnąć jeszcze więcej rekordów niż dwa miliony by zobaczyć próg możliwości samego serwera.
Dla więcej rekordów niż dwa miliony serwer IIS po prostu się podawał i zwracał wyjątek “System.OutOfMemoryException”.
Określiłem też ,że to ograniczenie wynika z serwera IIS, ponieważ jak widać wolnej pamięci RAM jeszcze trochę mamy. Z drugiej strony nie oszukujmy się próbujemy przy jednym zapytaniu wykonać operacje na rekordach o absurdalnej liczbie więc nie powinnyśmy się dziwić ,że serwer mówi spadaj.
Gdy liczbę rekordów zmniejszyłem do dwóch milionów oczywiście napotkałem na kolejny wyjątek. Jest on mi dosyć dobrze znany, ponieważ przekroczenie domyślnej długości wiadomości w WCF czasem nie jest ,aż takie trudne.
Jak ten problem rozwiązać oczywiście edytujemy web.configa i zwiększamy parametry takie jak “maxReceivedMessageSize” do maksymalnej liczby “int”.
<?xml version="1.0"?>
<configuration>
<system.web>
<compilation debug="true" targetFramework="4.5" />
<httpRuntime targetFramework="4.5" />
</system.web>
<appSettings>
<add key="DataPath" value="D:\PanNiebieski\Pulpit\imaginary_data.csv"/>
</appSettings>
<system.serviceModel>
<services>
<service name="WCF2Millions.DataService">
<endpoint address="" binding="wsHttpBinding" bindingConfiguration="My"
contract="WCF2Millions.IDataService" />
</service>
</services>
<bindings>
<wsHttpBinding>
<binding name="My" maxBufferPoolSize="2147483647" maxReceivedMessageSize="2147483647" />
</wsHttpBinding>
</bindings>
<behaviors>
<serviceBehaviors>
<behavior name="">
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="false" />
<dataContractSerializer maxItemsInObjectGraph="2147483647"/>
</behavior>
</serviceBehaviors>
</behaviors>
<serviceHostingEnvironment multipleSiteBindingsEnabled="true" aspNetCompatibilityEnabled="true"/>
</system.serviceModel>
</configuration>
No to teraz nie powinno być żadnych problemów prawda w końcu zwiększyłem wszystko co się tylko dało do maksymalnej wartości liczby całkowitej w .NET.
Od tej pory po stronie serwera nie było już żadnych problemów.
Jednak zawsze może wystąpić problem z przesłaniem tych 2 milionów rekordów do klienta. Nie żeby się tego nie spodziewał.
Na Stack Overflow jeszcze przeczytałem ,że być może problem leży w ograniczeniu długości zapytania więc go przedłużyłem do 64 MB.
<configuration>
<system.web>
<compilation debug="true" targetFramework="4.5" />
<httpRuntime targetFramework="4.5" />
<httpRuntime maxRequestLength="64384" />
Dla odmiany otrzymany jeszcze inny wyjątek. Klient ma problem deserializacją danych, ponieważ zostały one źle serializowane. Postanowiłem też usunąć tą linijkę kodu w web.config, ponieważ miałem dziwne problemy z dalszym testowaniem kodu.
Teraz wiemy ,że 2 miliony rekordów normalnie przez WCF-a nie przejdzie. Sprawdzimy, od jakiej liczby usługa sieciowa zacznie działać.
Dla pół miliona klient wyrzucał wyjątek “OutOfMemoryException”. Wyjątek ten pojawia się w nieokreślonym miejscu. Mogę tylko zakładać ,że deserializacja pół miliona elementów nie działa z powodu ograniczenia pamięciowego wewnątrz .NET. Pamięci RAM miałem jeszcze sporo.
Dla “200.000” rekordów usługa sieciowa już działa. Jednak chcemy przesłać 2 miliony rekordów, czyli 10 razy więcej. Co można z takim fantem zrobić.
No cóż, na pewno jednym ciągiem nie prześlemy 2 milionów rekordów. Istnieją pewne techniki, które pozwalają ten problem rozwiązać ,a przynajmniej mam taką nadzieje, ponieważ jeszcze nie próbowałem ich dla takiego masochistycznego scenariusza.
Będzie ciekawie.