SharePoint mimo wielu (no kilku) swoich zalet, ma też dużo wad. Jedną z nich napewno są developerzy, którzy zabierają się do pisania kodu nie czytając dostępnych technicznych dokumentów na temat tworzenia rozwiązań na SharePoint.
Jednym z klasycznych błędów jest założenie, że lista może zawierać N elementów i wciąż będzie działała jak należy. Nie ma nic bardziej mylnego! SharePoint pozwala trzymać w jednym kontenerze do 2 tys. elementów – nie jest to stricte ograniczenie, bo do listy możecie dodać nawet 20-30 tysięcy elementów, ale jest to ograniczenie, po którym MS nie gwarantuje, że lista będzie działać wydajnie. Ba, nawet jest udukomentowany test, który pokazuje że wraz ze wzrostem elementów na liście wydajność takiej listy maksymalnie spada. Zachęcam więc wszystkich do lektury, bo warto o tym wiedzieć. Także warto wiedzieć, że w takich przypadkach nie wykożystuje się iteratora foreach na wszystkich elementach listy, tylko np.: przez SPQuery lub PortalSiteMapProvider.
Jednakże wystarczy tylko zrozumieć co to jest kontener, i automatycznie na liście można przechowywać do 5 milionów elementów bez wpłuwu na jej wydajność. A więc, co to takiego jest kontener? Kontener na liście to nic innego jak folder. Czyli, nasza lista może składać się z 2K kontenerów, które zawierają 2K kontenerów, które zawierają 2K elementów itd. Dzięki takiemy podejściu strona z listą i elementami na liście powinna w miarę szybko i sprawnie się renederować. Także dostęp poprzez SPList.Items będzie szybszy i wydajniejszy.
Ale dlaczego piszę o tym wszystkim w kontekście uprawnień? Bo z nimi jest dosłownie tak samo, z tą różnicą, że jeżeli przekroczycie magiczne 2K na Site to SharePoint daje wam krzyżyk na drogę. Kontenerem w tym wypadku jest grupa SharePoint, która może zawierać zarówno użytkowników jak i grupy AD. W obydwu przypadkach liczba grup i użytkowników w danym kontenerze nie powinna przekraczać 2K. Jak tylko przekroczy ona tą magicznę liczbę, to jakakolwiek próba usunięcia, dodania czy nawet modyfikacji uprawnienia dla użytkownika na danej stronie w zależności od maszyny (zwykły 8 corowy Xeon i 9 GB ramu należy w tym przypadku do słabego sprzętu) na której stoi SharePoint może zakończyć się timeoutem, ba może też spowodować deadlocki na bazie danych.
Dlaczego tak się dzieje? Jak tylko użytkownik zaloguje się na stronę SharePoint po raz pierwszy, jego wpis trafia do bazy danych UserInfo, która to zawiera takie dane jak login, nazwę, ID itp. Jest to tak naprawdę podstawa, to tam są trzymane wszystkie dane dot. użytkownika. Następnie SharePoint dodaje użytkownika do tabeli RoleAssgiments w której przypisuje użytkownika do odpowiedniej roli na odpowiednim site i web. Jak już to wszystko zostało dodane, SharePoint aktualizuje tabelę Perms i jej Acl dla odpowiedniego Scope. Do póki bawimy się uprawnieniamy na SharePoint w Site/Web Scope to wszystko jest wporządku i raczej nie przekroczy się liczby 2K – skoro działa się tak ogólnie to można potworzyć grupy i nadać uprawnienia grupom. Problem zaczyna się gdy na pewnej liście potrzebujemy unikatowych uprawnień per użytkownik.
Dodanie unikatowe uprawnienia dla użytkownika dla kontrektego elementu, powoduje, że SharePoint propaguje to uprawnienie do listy i następnie do Web. Będzie ono figurowało jako Limited Access, ale wpisy się dokonają. Tym razem zostanie dodany wpis w Perms dla elementu, zostanie zaktualizowany perms dla listy i dla webu, dodatkowo nowy wpis pojawi się w RoleAssigment by zaznaczyć że dla danego Scope użytkownik ma inne uprawnienia. Teraz jeżeli trochę sobie policzymy, to wartości w Perms rosną proporcjonalnie do elmentów do których przypisujemy uprawnienia. Jeżeli teraz nadamy uprawnienia 2K osobom, to nasze tabele RoleAssigment i Perms rozrosną się o łączną wartość okolo 4K elmentów, a jeżeli mamy włączone dziedziczenie, to z propaguje to się także na inne listy. Czyli 4K jest minimalnym zwrotem liczby elementów w bazie danych. Jeżeli teraz to pociągniemy dalej to liczby bardzo szybko mogą sięgać milionów rekordów. Mimo, że SQL Server poradzi sobie z taką liczbą elementów to SharePoint już nie, a tym bardziej nie poradzą sobie procedury wbudowane od SharePoint. Spowodowane jest to tym, że SharePoint w procedurze proc_SecRemovePrincipalFromOneScope wywołuje za pomocą kursora proc_SecUpdateAclForScope, która za pomocą kursora aktualizuje wszystkie RoleAssgiments spełniające warunki SiteId, WebId i ScopeId. Zaś proc_SecRemovePrincipalFromOneScope jest wywoływana w kursorze w proc_SecRemovePrincipalFromScope, która robi iteracje po wszystkich Perms. I teraz przy dużej liczbie elementów w tabeli RoleAssgiments dla danego Site i Web, takie proste usunięcie jednego użytkownika z listy uprawnień może trwać ponad godzinę jak nie więcej (ja w ramach testów rezygnowałem po 40 minutach czekania). Nie wspominając, że wykonanie tego ze strony po 4-5 minutach spowoduje timeout. Następnie, przeważnie klikamy by usunać użytkownika jeszcze raz itd. aż baza danych się nam wykrzaczy :)
Wyjściem z sytuacji jest usuwanie ręczne użytkowników z bazy danych, a dokładnie mówiąc, usunięcie użytkowników z RoleAssigments dla odpowiednich scope – scope ID można popbrać poprzez np.: zapytanie:
SELECT ScopeId, ScopeUrl FROM Perms WHERE SiteId = @SiteId AND WebId = @WebId Lub porpzez: SELECT ScopeId, ScopeUrl FROM Perms WHERE ScopeUrl = 'strona/itp/itd'
Dzięki temu, dowiemy się jakie istnieją ScopeId, z których użytkownicy powinni zostać usunięci. Następnie znajać już ScopeID wykonujemy operacje DELETE na RoleAssigments.
Możemy także zapytanie połaczyć i stworzyć coś takiego:
SELECT ScopeId INTO #tempPermsGutek FROM Perms WHERE SiteId = '79419D7E-C83E-4520-8D3F-825DB3C28790' AND WebId = '1588250E-4FEB-4369-B831-5C011B871CAF' AND Perms.ScopeUrl IN ( SELECT ScopeUrl FROM Perms WHERE SiteId = '79419D7E-C83E-4520-8D3F-825DB3C28790'AND ( ScopeUrl LIKE 'rp/Lists/Submi%' OR ScopeUrl LIKE 'rp/Lists/Task%' ) ) DELETE RoleAssignment FROM RoleAssignment INNER JOIN #tempPermsGutek ON RoleAssignment.ScopeId = #tempPermsGutek.ScopeId WHERE RoleAssignment.SiteId = '79419D7E-C83E-4520-8D3F-825DB3C28790' AND RoleAssignment.PrincipalId NOT IN ( -- NUMERY ID OSOB, KTORYCH NIE CHCEMY KASOWAÆ )
Kiedy już mamy pewność, że tabela RoleAssigments została oczyszczona – to znaczy zawiera odpowiednio mało rekordów (9 milionów w moim przypadku było mało), wykonujemy następujący kod:
DECLARE @scopeId uniqueidentifier DECLARE cur CURSOR FOR SELECT ScopeId FROM #tempPermsGutek OPEN cur FETCH NEXT FROM cur INTO @scopeId WHILE (@@FETCH_STATUS = 0) BEGIN EXEC proc_SecUpdateAclForScope '79419D7E-C83E-4520-8D3F-825DB3C28790' , @scopeId FETCH NEXT FROM cur INTO @scopeId END CLOSE cur DEALLOCATE cur
Teraz by mieć pewność, że nie zostawiliśmy żadnego smordu w bazie danych, a użytkoników, których kasowaliśmy kasowaliśmy z poziomu Web, upewniamy się, że nie widnieją oni w tabelach:
- EventSubsMatches
- ImmedSubscriptions
- SchedSubscriptions
- Personalization
- WebParts
- WebMembers
Jeżeli widnieją, to wykonujemy następujące zapytanie:
DECLARE @SiteId uniqueidentifier, DECLARE @WebId uniqueidentifier, DECLARE @WebScopeId uniqueidentifier, DECLARE @ScopeId uniqueidentifier, DECLARE @PrincipalId int DELETE FROM EventSubsMatches WHERE SubId IN ( SELECT ImmedSubscriptions.Id FROM Webs, ImmedSubscriptions WHERE ImmedSubscriptions.SiteId = Webs.SiteId AND ImmedSubscriptions.WebId = Webs.Id AND ImmedSubscriptions.UserId = @PrincipalId AND Webs.SiteId = @SiteId AND Webs.FirstUniqueAncestorWebId = @WebId UNION SELECT SchedSubscriptions.Id FROM Webs, SchedSubscriptions WHERE SchedSubscriptions.SiteId = Webs.SiteId AND SchedSubscriptions.WebId = Webs.Id AND SchedSubscriptions.UserId = @PrincipalId AND Webs.SiteId = @SiteId AND Webs.FirstUniqueAncestorWebId = @WebId ) DELETE ImmedSubscriptions FROM Webs, ImmedSubscriptions WHERE ImmedSubscriptions.SiteId = Webs.SiteId AND ImmedSubscriptions.WebId = Webs.Id AND ImmedSubscriptions.UserId = @PrincipalId AND Webs.SiteId = @SiteId AND Webs.FirstUniqueAncestorWebId = @WebId DELETE SchedSubscriptions FROM Webs, SchedSubscriptions WHERE SchedSubscriptions.SiteId = Webs.SiteId AND SchedSubscriptions.WebId = Webs.Id AND SchedSubscriptions.UserId = @PrincipalId AND Webs.SiteId = @SiteId AND Webs.FirstUniqueAncestorWebId = @WebId DELETE Personalization FROM Personalization INNER JOIN Docs ON Personalization.tp_PageUrlID = Docs.Id WHERE Personalization.tp_SiteId = @SiteId AND Personalization.tp_UserID = @PrincipalId AND Docs.SiteId = @SiteId AND Docs.WebId = @WebId DELETE WebParts FROM WebParts INNER JOIN Docs ON WebParts.tp_PageUrlID = Docs.Id WHERE WebParts.tp_SiteId = @SiteId AND WebParts.tp_UserID = @PrincipalId AND Docs.SiteId = @SiteId AND Docs.WebId = @WebId DELETE FROM WebMembers WHERE WebId = @WebId AND UserId = @PrincipalId
I już na samym końcu wywołujemy:
EXEC proc_SecUpdateSiteLevelSecurityMetadata @SiteId , 1, 0 EXEC proc_GetWebIdAuditMask @SiteId, @WebId
Teraz możemy ponownie rozkosztować się stroną SharePoint, aż do następnego razu :)
Cały kod, który tutaj wkleiłem postaram się ubrać w odpowiednie procedury wbudowane, dzięki czemu będzie z niego łatwiej skożystać.
Gutek, przedstawiłeś bardzo ciekawy problem. Wielu problemów nie byłem świadomy. Dzięki :)
Ciekawe rzeczy piszesz ! Tylko (ta czesc komentarza do skasowania): wykożystuje , usówanie – orty
10/10 – super! Więcej o SharePoint proszę!
Jedno pytanie. Czy zalecane maksymalne samoograniczenie dotyczy listy jako takiej, czy tylko widoku? Chodzi mi o sytuację, iż powiedzmy na liście mamy 10 000 elementów, ale widok domyślny zwraca nam ot 100.
@Ogolnie
Tak sie pochlonalem mysla o security principals, ze nie zwrocilem uwage na swoj maly blad! z gory za niego przepraszam. Na listach jest ograniczenie do 2K elementow w kontenerze – tekst poprawiony.
Dodatkowo zmienilem security principals na 2K – taka liczbe MS wspiera z zgodnie z ta informacja jaka dostalem na maila. Ale takze jest potwierdzone ze do 5K security principals SharePoint powinien wytrzymac. Z moich testow wynika za na 8 corowym Xenonie i 9GB ramu WSS 3.0 wytrzymal obciazenie 5K unikatowych uprawnien dla uzytkownikow, przy minimalnym opoznieniu nadawania/odbierania uprawnien na listach – zamiast pol sekundy, trwa to okolo 20/30 ale sie nie wywala.
Z gory przepraszam za zamieszanie z liczbami :(
@Tomasz
Dzieki :) i sorki, juz instaluje polski słownik ;)
@Arkadiusz
Ogolnie jest tak, do poki nie kozystasz z API to jestes wskazany na View. Jezeli ograniczasz View do pierwszych 100 elementow, to bedzie ono sie szybciej renderowalo niz bys nie ograniczal do 100, jednak performance issue dalej bedzie istnialo. Ograniczenie View do 100 elementow to tak naprawde zapytanie CAML, czyli wykonanie SPQuery. Zgodnie z wynikami testow MSowych to bedzie dzialalo wydajniej niz pokazanie wszystkich elementow na stronie :)
Że szybciej pokaże 100 niż 10000 to wiadomo ;-) Chodzi o to jak jest wewnętrznie traktowane. Z tego wychodzi, że trzeba być świadomi ogranciczeń i można szaleć.
@Arkadiusz
To tak jak pisalem, robi SPQuery, ktore dziala szybciej niz renderowanie strony ze wszystkimi elementami. Jednak zalecalbym dzielenie wszystkiego na kontenery a co jakis czas archiwizowanie starych elementow. Dzieki czemu zachowa sie wydajnosc listy.
Super, więcej takich tekstów!
Witam
Ciekawy problem opisałeś i miałbym pytanie odnośnie uprawnień. Otóż mam taką sytuację że chciałbym zmieniać uprawnienia jednej grupy użytkowników(max 50 userów) do poszczególnych elementów listy, np. mam listę “Zamówienia” i jeżeli jest początek roku 2009 chciałbym zabrać uprawnienia edycji do elementów które np mają datę_zamówienia=2008 (żeby zostało tylko prawo do podglądu takiego zamówienia). Jeszcze nie wiem jak to zrobię:) Czy to ograniczenie unikatowych uprawnień wówczas też by się tutaj odnosiło, np. jakbym zmieniał uprawnienia do 4K elementów rocznie. Pozdrawiam
@MadMar
sorki, ale ostatnio mam ograniczony dostep do sieci.
Tak, w takich wypadkach tez ograniczenie 4K ma miejsce.
Mozna sie zastanowic nad “Archiwizowaniem” takich elementow na inna liste gdzie tylko jest prawo read do wszystkich elementow, moze to zalatwi problem. IMHO najlepiej nie wykorzystywac unique permissions chyba ze jest to niezbedne. Mozna zrobic wlasna impelmentacje zabezpieczen poprzez listy posrednie i kolumny na listach docelowych z lookupem i odpowiednim view. Takze mozna potworzyc rozne delegate controls ktore beda blokowaly wejscie gdy osoba nie ma dostepu. Jest to pracochlonne ale wydejnieszje rozwiazanie – w czasie na pewno wydajnieszje, na poczatku moze byc wolniejsze.
Gutek
Witam
Dzięki za odpowiedź.
Czyli rozumiem, że lepiej nie bawić się w unikalne uprawnienia do poszczególnych elementów listy (przynajmniej w ilościach po kilka tys. na listę).
Wymyśliłem inny sposób aby to zrobić. Otóż napisałem event handler, który ustawia atrybut, że element jest archiwalny (w jakiejs kolumnie, np. Archiw i ten ev hand wywoływany jest z innej listy), a następnie 2 event handler (ustawiony już na tej właściwej liście) pilnuje aby nie można było edytować lub usuwać zamówień, które mają ustawiony ten atrybut. Nie wiem czy to dobre podejście…
Pozdrawiam
@MadMar
Tak naprawdę kazde podejscie jest dobre :) w zaleznosci od wymagan. Mozesz zamiast Event Receiver’a zrobic Timer Job, ktory bedzie dzialal raz na rok i wykonywal ta operacje automatycznie. Bardzo dobry artykul na temat tworzenia Timer Job mozna znalezc tutaj:
http://msdn.microsoft.com/en-us/library/cc406686.aspx
Innym sposobem jest ustawienie policy na auto archiwizacje dokumentow/elementow starszych niz X:
http://office.microsoft.com/en-us/sharepointserver/HA101544291033.aspx
Wazne by uzytkownicy nie odczuli tego iz cos sie dzieje w tle. Event Receiver moze to powodowac, ale tez nie musi :)
Ja osobiscie bym to rozwiazal na dwoch listach:
1) pierwsza dla aktualnych elementow
2) druga dla archiwalnych
i napisal web part do wyswietlania wszystkich lub wybranych – albo jezeli nie chcesz pisac wyklikalbym go z SPD (choc to by troche zajelo).
Pozdrawiam,
Gutek
Comments are closed.