понедельник, 30 мая 2011 г.

Странный DirectoryServices. Совет первый: выражайтесь яснее, где искать

В прошлый раз я вскользь упомянул про подводные камни, встречающиеся на пути программиста, решившего использоваться классы пространства имён System.DirectoryServices или DCOM объекты ADSI, что, по сути, то же самое. Данная тема заслуживает отдельного рассмотрения. Поэтому эту и несколько следующих заметок я посвящу ей.
Сегодня я хочу рассмотреть весьма распространённую ошибку, связанную с особенностью выбора контроллера домена, на котором будет осуществляться поиск. В большинстве случаев программист полагается на настройки по умолчанию и может получить существенное падение производительности при определённых конфигурациях. Самое неприятное то, что отследить такую ошибку на этапе тестирования очень сложно.

Как делает большинство?
Рассмотрим стандартный код поиска с помощью классов из пространства имён System.DirectoryServices:
DirectoryEntry rootDSE = new DirectoryEntry("LDAP://rootDSE");
string domainContextLDAP = "LDAP://"
    + rootDSE.Properties["rootDomainNamingContext"].Value;

String[] properties = new String[] {
    "objectGUID",
    "displayName",
    "givenName",
    "sn" };
DirectorySearcher searcher = new DirectorySearcher(
    new DirectoryEntry(domainContextLDAP),
    "(objectClass=person)",
    properties,
    SearchScope.Subtree);

using (SearchResultCollection searchResultCol
    = searcher.FindAll())
{
// ...
}
Всё в порядке? Нет, нет и нет! В данном коде пропущена существенная деталь – не указан контроллер домена, на котором будет выполняться поиск. В этом случае может быть задействован любой контроллер домена в текущем сайте. И абсолютно не обязательно, что это будет ближайший к вам сервер. Сайт в большой организации может покрывать несколько офисов в пределах города, а каждый офис может иметь свой контроллер домена – любой из них может быть выбран для выполнения операции поиска. Вес и приоритет SRV записей домена могут повлиять на результат выбора, но в любом случае этот параметр не подлежит конфигурированию на стороне клиента и потому полагаться на него не стоит. Опасность заключается в том, что данный код вполне себе работоспособен, и наблюдать падение производительности вы можете лишь на определённых конфигурациях, причём характерных весьма большим предприятиям. Поэтому отдел контроля качества легко может пропустить данную проблему.

Мы пойдём другим путём.
Но постойте, как же указать сервер, если ни класс DirectoryEntry, ни класс DirectorySearcher не содержат в своих интерфейсах подобного свойства? Всё очень просто. В данном примере для инициализации объекта типа DirectoryEntry применяется так называемый serverless формат записи LDAP пути: LDAP://CN=Users,DC=neverhood,DC=org. Полный LDAP путь позволяет указать не только адрес или имя контроллера домена, но даже порт, на котором слушает сервис:
LDAP://[HostName[:Port]/]DistinguishedName
Пример: LDAP://dc1-nsib.neverhood.org/CN=Users,DC=neverhood,DC=org.

Содержимое объекта rootDSE чуть более предсказуемо, чем результат выбора контроллера домена по-умолчанию. И что более важно, оно зависит от контекста безопасности текущего потока, а не от настроек приоритетов серверов. В частности свойство dnsHostName содержит адрес контроллера домена, через который был осуществлён вход в систему. А это в свою очередь поддаётся конфигурированию на клиенте. И можно надеяться, что при грамотно настроенной конфигурации, сервер, используемый по умолчанию для входа в систему, всегда ближайший. Поэтому код, вычисляющий контекст поиска можно переписать следующим образом:
DirectoryEntry rootDSE = new DirectoryEntry("LDAP://rootDSE");
string domainContextLDAP = "LDAP://"
    + rootDSE.Properties["dnsHostName"].Value.ToString() + "/"
    + rootDSE.Properties["rootDomainNamingContext"].Value;

Чтобы не быть голословным, напоследок приведу простые замеры. Для тестирования использовалась топология, в которой сайт охватывал несколько территориально распределённых офисов. Каждый из офисов содержал свой контроллер доменов. Рабочая машина была сконфигурирована таким образом, что для входа в систему использовался ближайший из них. Приложение выполняла поиск и перебор всех объектов в домене. В первом случае использовалось serverless связывание, во втором контроллер домена указывался явно:
Search context: LDAP://DC=neverhood,DC=org
Time left: 00:00:24.5233572
Entities number: 6234
Press any key...

Search context: LDAP://dc1-nsib.neverhood.org/DC=neverhood,DC=org
Time left: 00:00:06.8716871
Entities number: 6234
Press any key...
По-моему цифры говорят сами за себя.

4 комментария:

Анонимный комментирует...

Другое дело, что этот контролер может быть опущен или не работоспособен по каким-то причинам. И придется усложнять логику.

Алексей Коротаев комментирует...

Именно поэтому и предлагается использовать тот контроллер домена, через который был осуществлён вход. С большой долей вероятности он будет жив. А вообще да, вы правы, - это всегда компромис. Либо усложняем логику и получаем шустрое приложение, либо полагаемся на стандартные механизмы и имеем простую логику, но в довесок получаем нарекания клиента.
Пример с замерами реальный, причём результат усреднён. В пределах значения были ещё более впечетляющие: 3 и 40 секунд. С моей точки зрения - это весьма неплохой аргумент в пользу усложнения.

Mykhaylo Khodorev комментирует...

Несколько офисов со связью, качество которой может повлиять на скорость доступа к AD, находящиеся в одном сайте - грубое нарушение принципов построения инфраструктуры AD. Так что по-моему лучше оставлять выбор контроллера по умолчанию.

Алексей Коротаев комментирует...

Обычно это мало волнует клиента, потому что сама операционная система не так часто обращается к AD и проблем со скоростью он не замечает, а проблемы с производительностью будут наблюдаться именно у вашего приложения. И ваше абсолютно правильное заявление о "грубом нарушении принципов построения инфраструктуры" будет выглядеть как оправдание и обвинение его системного администратора в некомпетентности, как вы думаете, кому поверят больше? Поэтому лучше использовать то значение, которое с максимальной долей вероятности будет наиболее оптимальным, чем полагаться на некий механизм определения по-умолчанию.
Кстати, явное указание контроллера доменов имеет под собой ещё один плюс: таким образом вы всегда будете осуществлять доступ через один и тот же сервер и не столкнётесь с проблемами репликации. Представьте себе, что первой операцией вы изменили свойство, а второй захотели его прочитать. Без явного указания контроллера доменов, ваши запросы при определнённых условиях могут пойти к разным серверам. Эффект от этого очень непредсказуемый. При решении задач автоматизации сторонних сервисов, мы в Parallels достаточно часто сталкивался с такой проблемой, чтобы навсегда расхотеть пользоваться механизмом поиска контроллера домена по умолчанию.

Отправить комментарий